@shapeshift-labs/frontier-mutation 0.1.0 → 0.1.2
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 +207 -2
- package/dist/index.d.ts +214 -10
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2198 -129
- package/dist/index.js.map +1 -1
- package/package.json +17 -5
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { OP_APPEND, OP_ARRAY_OBJECT_FIELD_ASSIGN, OP_ASSIGN, OP_SET, OP_STRING_SPLICE, applyPatch, cloneJson, getPath, parsePointer } from '@shapeshift-labs/frontier';
|
|
1
|
+
import { OP_APPEND, OP_ARRAY_MOVE, OP_ARRAY_OBJECT_FIELD_ASSIGN, OP_ARRAY_SPLICE, OP_ASSIGN, OP_REMOVE, OP_SET, OP_STRING_SPLICE, applyPatch, cloneJson, diff, getPath, parsePointer } from '@shapeshift-labs/frontier';
|
|
2
|
+
const DEFAULT_DIRTY_DIFF_MIN_SELECTIVITY = 0.75;
|
|
2
3
|
export class SelectorBuilder {
|
|
3
4
|
plan;
|
|
4
5
|
constructor(path) {
|
|
@@ -25,21 +26,95 @@ export class SelectorBuilder {
|
|
|
25
26
|
return this;
|
|
26
27
|
}
|
|
27
28
|
keyBy(key) {
|
|
28
|
-
this.plan.keyBy = key;
|
|
29
|
+
this.plan.keyBy = normalizePath(key, 'selector keyBy path');
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
indexBy(key) {
|
|
33
|
+
this.plan.indexBy = normalizePath(key === undefined ? this.plan.keyBy || '$key' : key, 'selector indexBy path');
|
|
34
|
+
return this;
|
|
35
|
+
}
|
|
36
|
+
orderBy(path, direction = 'asc') {
|
|
37
|
+
if (direction !== 'asc' && direction !== 'desc')
|
|
38
|
+
throw new TypeError('selector orderBy direction must be asc or desc');
|
|
39
|
+
this.plan.orderBy = { path: normalizePath(path, 'selector orderBy path'), direction };
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
limit(count) {
|
|
43
|
+
this.plan.limit = normalizeIndex(count, 'selector limit');
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
first() {
|
|
47
|
+
return this.limit(1);
|
|
48
|
+
}
|
|
49
|
+
project(...fields) {
|
|
50
|
+
if (fields.length === 0)
|
|
51
|
+
throw new TypeError('selector project requires at least one field');
|
|
52
|
+
this.plan.project = fields.map((field) => normalizePath(field, 'selector project path'));
|
|
53
|
+
return this;
|
|
54
|
+
}
|
|
55
|
+
named(name) {
|
|
56
|
+
this.plan.name = normalizeSelectorName(name);
|
|
29
57
|
return this;
|
|
30
58
|
}
|
|
31
59
|
toPlan() {
|
|
32
60
|
return cloneSelectorPlan(this.plan);
|
|
33
61
|
}
|
|
34
62
|
}
|
|
63
|
+
export class SelectorRegistry {
|
|
64
|
+
selectors = new Map();
|
|
65
|
+
constructor(initial) {
|
|
66
|
+
if (initial === undefined)
|
|
67
|
+
return;
|
|
68
|
+
if (Symbol.iterator in Object(initial)) {
|
|
69
|
+
for (const [name, selector] of initial) {
|
|
70
|
+
this.define(name, selector);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
else {
|
|
74
|
+
for (const name of Object.keys(initial)) {
|
|
75
|
+
this.define(name, initial[name]);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
define(name, selector) {
|
|
80
|
+
const normalizedName = normalizeSelectorName(name);
|
|
81
|
+
const plan = selector instanceof SelectorBuilder ? selector.toPlan() : cloneSelectorPlan(selector);
|
|
82
|
+
plan.name = normalizedName;
|
|
83
|
+
this.selectors.set(normalizedName, plan);
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
get(name) {
|
|
87
|
+
const normalizedName = normalizeSelectorName(name);
|
|
88
|
+
const selector = this.selectors.get(normalizedName);
|
|
89
|
+
if (selector === undefined)
|
|
90
|
+
throw new TypeError('unknown selector: ' + normalizedName);
|
|
91
|
+
return cloneSelectorPlan(selector);
|
|
92
|
+
}
|
|
93
|
+
has(name) {
|
|
94
|
+
return this.selectors.has(normalizeSelectorName(name));
|
|
95
|
+
}
|
|
96
|
+
entries() {
|
|
97
|
+
return Array.from(this.selectors, ([name, selector]) => [name, cloneSelectorPlan(selector)]);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
35
100
|
export class MutationPlan {
|
|
36
101
|
ops = [];
|
|
37
102
|
currentSelector;
|
|
103
|
+
transactionStack = [];
|
|
38
104
|
get operations() {
|
|
39
105
|
return this.ops;
|
|
40
106
|
}
|
|
41
|
-
forEach(selector) {
|
|
107
|
+
forEach(selector, scope) {
|
|
108
|
+
const previousSelector = this.currentSelector;
|
|
42
109
|
this.currentSelector = selector instanceof SelectorBuilder ? selector.toPlan() : cloneSelectorPlan(selector);
|
|
110
|
+
if (scope !== undefined) {
|
|
111
|
+
try {
|
|
112
|
+
scope(this);
|
|
113
|
+
}
|
|
114
|
+
finally {
|
|
115
|
+
this.currentSelector = previousSelector;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
43
118
|
return this;
|
|
44
119
|
}
|
|
45
120
|
where(path, condition) {
|
|
@@ -55,6 +130,18 @@ export class MutationPlan {
|
|
|
55
130
|
set(path, value) {
|
|
56
131
|
return this.push({ kind: 'set', path: normalizePath(path, 'set path'), value, repeat: 1 });
|
|
57
132
|
}
|
|
133
|
+
unset(path) {
|
|
134
|
+
return this.push({ kind: 'unset', path: normalizePath(path, 'unset path'), repeat: 1 });
|
|
135
|
+
}
|
|
136
|
+
remove(path) {
|
|
137
|
+
return this.push({ kind: 'remove', path: normalizePath(path, 'remove path'), repeat: 1 });
|
|
138
|
+
}
|
|
139
|
+
ensure(path, defaultValue) {
|
|
140
|
+
return this.push(makeValueOperation('ensure', normalizePath(path, 'ensure path'), defaultValue));
|
|
141
|
+
}
|
|
142
|
+
upsert(path, value) {
|
|
143
|
+
return this.push(makeValueOperation('upsert', normalizePath(path, 'upsert path'), value));
|
|
144
|
+
}
|
|
58
145
|
assign(path, value) {
|
|
59
146
|
return this.push({ kind: 'assign', path: normalizePath(path, 'assign path'), value, repeat: 1 });
|
|
60
147
|
}
|
|
@@ -64,6 +151,22 @@ export class MutationPlan {
|
|
|
64
151
|
decrement(path, delta = 1) {
|
|
65
152
|
return this.push({ kind: 'decrement', path: normalizePath(path, 'decrement path'), delta: normalizeInteger(delta, 'decrement delta'), repeat: 1 });
|
|
66
153
|
}
|
|
154
|
+
multiply(path, factor) {
|
|
155
|
+
return this.push({ kind: 'multiply', path: normalizePath(path, 'multiply path'), delta: normalizeFiniteNumber(factor, 'multiply factor'), repeat: 1 });
|
|
156
|
+
}
|
|
157
|
+
min(path, value) {
|
|
158
|
+
return this.push({ kind: 'min', path: normalizePath(path, 'min path'), delta: normalizeFiniteNumber(value, 'min value'), repeat: 1 });
|
|
159
|
+
}
|
|
160
|
+
max(path, value) {
|
|
161
|
+
return this.push({ kind: 'max', path: normalizePath(path, 'max path'), delta: normalizeFiniteNumber(value, 'max value'), repeat: 1 });
|
|
162
|
+
}
|
|
163
|
+
clamp(path, min, max) {
|
|
164
|
+
const normalizedMin = normalizeFiniteNumber(min, 'clamp min');
|
|
165
|
+
const normalizedMax = normalizeFiniteNumber(max, 'clamp max');
|
|
166
|
+
if (normalizedMin > normalizedMax)
|
|
167
|
+
throw new RangeError('clamp min must be less than or equal to clamp max');
|
|
168
|
+
return this.push({ kind: 'clamp', path: normalizePath(path, 'clamp path'), min: normalizedMin, max: normalizedMax, repeat: 1 });
|
|
169
|
+
}
|
|
67
170
|
toggle(path) {
|
|
68
171
|
return this.push({ kind: 'toggle', path: normalizePath(path, 'toggle path'), repeat: 1 });
|
|
69
172
|
}
|
|
@@ -71,6 +174,10 @@ export class MutationPlan {
|
|
|
71
174
|
const normalizedValues = Array.isArray(values) ? values : [values];
|
|
72
175
|
return this.push({ kind: 'append', path: normalizePath(path, 'append path'), values: normalizedValues, repeat: 1 });
|
|
73
176
|
}
|
|
177
|
+
prepend(path, values) {
|
|
178
|
+
const normalizedValues = Array.isArray(values) ? values : [values];
|
|
179
|
+
return this.push({ kind: 'prepend', path: normalizePath(path, 'prepend path'), values: normalizedValues, repeat: 1 });
|
|
180
|
+
}
|
|
74
181
|
splice(path, start, deleteCount, values = []) {
|
|
75
182
|
return this.push({
|
|
76
183
|
kind: 'splice',
|
|
@@ -81,6 +188,48 @@ export class MutationPlan {
|
|
|
81
188
|
repeat: 1
|
|
82
189
|
});
|
|
83
190
|
}
|
|
191
|
+
insert(path, index, values) {
|
|
192
|
+
const normalizedValues = Array.isArray(values) ? values : [values];
|
|
193
|
+
return this.push({
|
|
194
|
+
kind: 'insert',
|
|
195
|
+
path: normalizePath(path, 'insert path'),
|
|
196
|
+
start: normalizeIndex(index, 'insert index'),
|
|
197
|
+
values: normalizedValues,
|
|
198
|
+
repeat: 1
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
removeAt(path, index, count = 1) {
|
|
202
|
+
return this.push({
|
|
203
|
+
kind: 'removeAt',
|
|
204
|
+
path: normalizePath(path, 'removeAt path'),
|
|
205
|
+
start: normalizeIndex(index, 'removeAt index'),
|
|
206
|
+
deleteCount: normalizeIndex(count, 'removeAt count'),
|
|
207
|
+
repeat: 1
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
moveItem(path, fromIndex, toIndex, count = 1) {
|
|
211
|
+
return this.push({
|
|
212
|
+
kind: 'moveItem',
|
|
213
|
+
path: normalizePath(path, 'moveItem path'),
|
|
214
|
+
start: normalizeIndex(fromIndex, 'moveItem fromIndex'),
|
|
215
|
+
toIndex: normalizeIndex(toIndex, 'moveItem toIndex'),
|
|
216
|
+
deleteCount: normalizeIndex(count, 'moveItem count'),
|
|
217
|
+
repeat: 1
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
addToSet(path, values) {
|
|
221
|
+
const normalizedValues = Array.isArray(values) ? values : [values];
|
|
222
|
+
return this.push({ kind: 'addToSet', path: normalizePath(path, 'addToSet path'), values: normalizedValues, repeat: 1 });
|
|
223
|
+
}
|
|
224
|
+
pull(path, values) {
|
|
225
|
+
const normalizedValues = Array.isArray(values) ? values : [values];
|
|
226
|
+
return this.push({ kind: 'pull', path: normalizePath(path, 'pull path'), values: normalizedValues, repeat: 1 });
|
|
227
|
+
}
|
|
228
|
+
removeWhere(path, predicate) {
|
|
229
|
+
if (typeof predicate !== 'function')
|
|
230
|
+
throw new TypeError('removeWhere predicate must be a function');
|
|
231
|
+
return this.push({ kind: 'removeWhere', path: normalizePath(path, 'removeWhere path'), predicate, repeat: 1 });
|
|
232
|
+
}
|
|
84
233
|
appendText(path, text) {
|
|
85
234
|
if (typeof text !== 'string')
|
|
86
235
|
throw new TypeError('appendText text must be a string');
|
|
@@ -98,6 +247,81 @@ export class MutationPlan {
|
|
|
98
247
|
repeat: 1
|
|
99
248
|
});
|
|
100
249
|
}
|
|
250
|
+
insertText(path, index, text) {
|
|
251
|
+
if (typeof text !== 'string')
|
|
252
|
+
throw new TypeError('insertText text must be a string');
|
|
253
|
+
return this.push({
|
|
254
|
+
kind: 'insertText',
|
|
255
|
+
path: normalizePath(path, 'insertText path'),
|
|
256
|
+
start: normalizeIndex(index, 'insertText index'),
|
|
257
|
+
text,
|
|
258
|
+
repeat: 1
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
deleteText(path, index, count) {
|
|
262
|
+
return this.push({
|
|
263
|
+
kind: 'deleteText',
|
|
264
|
+
path: normalizePath(path, 'deleteText path'),
|
|
265
|
+
start: normalizeIndex(index, 'deleteText index'),
|
|
266
|
+
deleteCount: normalizeIndex(count, 'deleteText count'),
|
|
267
|
+
text: '',
|
|
268
|
+
repeat: 1
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
replaceText(path, index, count, text) {
|
|
272
|
+
if (typeof text !== 'string')
|
|
273
|
+
throw new TypeError('replaceText text must be a string');
|
|
274
|
+
return this.push({
|
|
275
|
+
kind: 'replaceText',
|
|
276
|
+
path: normalizePath(path, 'replaceText path'),
|
|
277
|
+
start: normalizeIndex(index, 'replaceText index'),
|
|
278
|
+
deleteCount: normalizeIndex(count, 'replaceText count'),
|
|
279
|
+
text,
|
|
280
|
+
repeat: 1
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
formatText(path, index, length, attributes) {
|
|
284
|
+
if (!isObjectRecord(attributes))
|
|
285
|
+
throw new TypeError('formatText attributes must be a JSON object');
|
|
286
|
+
return this.push({
|
|
287
|
+
kind: 'formatText',
|
|
288
|
+
path: normalizePath(path, 'formatText path'),
|
|
289
|
+
start: normalizeIndex(index, 'formatText index'),
|
|
290
|
+
length: normalizeIndex(length, 'formatText length'),
|
|
291
|
+
attributes: cloneJson(attributes),
|
|
292
|
+
repeat: 1
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
move(from, to) {
|
|
296
|
+
return this.push({ kind: 'move', path: normalizePath(from, 'move from path'), to: normalizePath(to, 'move to path'), repeat: 1 });
|
|
297
|
+
}
|
|
298
|
+
copy(from, to) {
|
|
299
|
+
return this.push({ kind: 'copy', path: normalizePath(from, 'copy from path'), to: normalizePath(to, 'copy to path'), repeat: 1 });
|
|
300
|
+
}
|
|
301
|
+
rename(path, newKey) {
|
|
302
|
+
if (typeof newKey !== 'string' && typeof newKey !== 'number')
|
|
303
|
+
throw new TypeError('rename key must be a string or number');
|
|
304
|
+
return this.push({ kind: 'rename', path: normalizePath(path, 'rename path'), key: newKey, repeat: 1 });
|
|
305
|
+
}
|
|
306
|
+
test(path, expected) {
|
|
307
|
+
return this.push({ kind: 'test', path: normalizePath(path, 'test path'), expected, repeat: 1 });
|
|
308
|
+
}
|
|
309
|
+
compareAndSet(path, expected, next) {
|
|
310
|
+
const op = makeValueOperation('compareAndSet', normalizePath(path, 'compareAndSet path'), next);
|
|
311
|
+
op.expected = expected;
|
|
312
|
+
return this.push(op);
|
|
313
|
+
}
|
|
314
|
+
transaction(scope, options = {}) {
|
|
315
|
+
const info = normalizeTransactionInfo(options);
|
|
316
|
+
this.transactionStack.push(info);
|
|
317
|
+
try {
|
|
318
|
+
scope(this);
|
|
319
|
+
}
|
|
320
|
+
finally {
|
|
321
|
+
this.transactionStack.pop();
|
|
322
|
+
}
|
|
323
|
+
return this;
|
|
324
|
+
}
|
|
101
325
|
repeat(count) {
|
|
102
326
|
const repeat = normalizeIndex(count, 'repeat count');
|
|
103
327
|
if (this.ops.length === 0)
|
|
@@ -108,8 +332,27 @@ export class MutationPlan {
|
|
|
108
332
|
compilePatch(state, options = {}) {
|
|
109
333
|
return compileMutationPlan(this, state, options);
|
|
110
334
|
}
|
|
335
|
+
commit(state, options) {
|
|
336
|
+
return commitMutation(state, this, options);
|
|
337
|
+
}
|
|
338
|
+
commitCrdt(doc, options) {
|
|
339
|
+
return commitCrdtMutation(doc, this, options);
|
|
340
|
+
}
|
|
341
|
+
explain(state, options = {}) {
|
|
342
|
+
const result = compileMutationPlan(this, state, options);
|
|
343
|
+
return {
|
|
344
|
+
...result,
|
|
345
|
+
operations: this.ops.map(cloneMutationOperation),
|
|
346
|
+
operationCount: this.ops.length,
|
|
347
|
+
patchOperationCount: result.patch.length
|
|
348
|
+
};
|
|
349
|
+
}
|
|
111
350
|
push(op) {
|
|
112
|
-
this.
|
|
351
|
+
const transaction = this.transactionStack.length === 0
|
|
352
|
+
? undefined
|
|
353
|
+
: cloneTransactionInfo(this.transactionStack[this.transactionStack.length - 1]);
|
|
354
|
+
const next = transaction === undefined ? op : { ...op, transaction };
|
|
355
|
+
this.ops.push(this.currentSelector === undefined ? next : { ...next, selector: cloneSelectorPlan(this.currentSelector) });
|
|
113
356
|
return this;
|
|
114
357
|
}
|
|
115
358
|
}
|
|
@@ -119,8 +362,17 @@ export function select(path) {
|
|
|
119
362
|
export function createMutationPlan() {
|
|
120
363
|
return new MutationPlan();
|
|
121
364
|
}
|
|
365
|
+
export function createSelectorRegistry(initial) {
|
|
366
|
+
return new SelectorRegistry(initial);
|
|
367
|
+
}
|
|
122
368
|
export function compileMutationPlan(plan, state, options = {}) {
|
|
123
369
|
const source = state === undefined ? null : state;
|
|
370
|
+
const planner = normalizePlannerOptions(options);
|
|
371
|
+
const operations = optimizeMutationOperations(plan.operations);
|
|
372
|
+
const runtime = createMutationRuntime(planner.schema);
|
|
373
|
+
if (planner.strategy === 'materialize-diff') {
|
|
374
|
+
return compileMaterializedDiffMutationPlan(operations, source, options, planner, runtime);
|
|
375
|
+
}
|
|
124
376
|
let working = cloneJson(source);
|
|
125
377
|
const patch = [];
|
|
126
378
|
const lowered = [];
|
|
@@ -128,52 +380,131 @@ export function compileMutationPlan(plan, state, options = {}) {
|
|
|
128
380
|
const dirtyRows = [];
|
|
129
381
|
const warnings = [];
|
|
130
382
|
const matchesOut = [];
|
|
383
|
+
const decisions = [];
|
|
131
384
|
const pendingRows = [];
|
|
132
385
|
let matched = 0;
|
|
133
|
-
for (const
|
|
134
|
-
const op = normalizeOperation(rawOp);
|
|
386
|
+
for (const op of operations) {
|
|
135
387
|
if (op.repeat === 0 || isNoopRepeat(op)) {
|
|
136
388
|
lowered.push(op.kind + '-repeat-noop');
|
|
389
|
+
decisions.push(makePlannerDecision(op, { strategy: 'noop', reason: 'repeat-noop' }));
|
|
137
390
|
continue;
|
|
138
391
|
}
|
|
139
392
|
if (op.selector !== undefined) {
|
|
140
|
-
const context = resolveSelector(working, op.selector);
|
|
141
|
-
matched += context.matches.length;
|
|
393
|
+
const context = resolveSelector(working, op.selector, runtime);
|
|
394
|
+
matched += context.matches.length * op.matchWeight;
|
|
142
395
|
for (const match of context.matches)
|
|
143
|
-
matchesOut.push(
|
|
396
|
+
matchesOut.push(makeSelectorMatchOut(match, context.selector));
|
|
144
397
|
if (context.matches.length === 0) {
|
|
145
398
|
lowered.push(op.kind + '-selector-empty');
|
|
399
|
+
decisions.push(makePlannerDecision(op, { strategy: 'noop', reason: 'selector-empty' }, context));
|
|
146
400
|
continue;
|
|
147
401
|
}
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
if (hasIncompatiblePendingRows(pendingRows, context))
|
|
402
|
+
const choice = chooseSelectorStrategy(planner, op, context);
|
|
403
|
+
decisions.push(makePlannerDecision(op, choice, context));
|
|
404
|
+
if (choice.strategy === 'row-field') {
|
|
405
|
+
if (hasIncompatiblePendingRows(pendingRows, context, context.tailPath.concat(op.path)))
|
|
152
406
|
flushPendingRows(pendingRows, patch, dirtyRows);
|
|
153
407
|
working = queueRowFieldMutation(pendingRows, context, op, working);
|
|
408
|
+
noteSelectorMutation(runtime, context, op);
|
|
154
409
|
lowered.push(op.kind + '-selector-row-field');
|
|
155
410
|
}
|
|
411
|
+
else if (choice.strategy === 'dirty-diff') {
|
|
412
|
+
flushPendingRows(pendingRows, patch, dirtyRows);
|
|
413
|
+
working = emitDirtySelectorDiffMutation(working, context, op, choice, patch, lowered, dirtyPaths, dirtyRows, planner);
|
|
414
|
+
noteSelectorMutation(runtime, context, op);
|
|
415
|
+
}
|
|
156
416
|
else {
|
|
157
417
|
flushPendingRows(pendingRows, patch, dirtyRows);
|
|
158
418
|
for (const match of context.matches) {
|
|
159
419
|
const absolutePath = match.path.concat(op.path);
|
|
160
420
|
const patchStart = patch.length;
|
|
161
|
-
emitAbsoluteMutation(working, absolutePath, op, patch, lowered, warnings);
|
|
421
|
+
const changedPaths = emitAbsoluteMutation(working, match.path, absolutePath, op, patch, lowered, warnings);
|
|
162
422
|
working = applyPatchOperationsToWorking(working, patch, patchStart);
|
|
163
|
-
|
|
423
|
+
for (const dirtyPath of changedPaths) {
|
|
424
|
+
dirtyPaths.push(dirtyPath);
|
|
425
|
+
noteAbsoluteMutation(runtime, dirtyPath);
|
|
426
|
+
}
|
|
164
427
|
}
|
|
165
428
|
}
|
|
166
429
|
continue;
|
|
167
430
|
}
|
|
168
431
|
flushPendingRows(pendingRows, patch, dirtyRows);
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
432
|
+
const choice = chooseAbsoluteStrategy(planner, op);
|
|
433
|
+
decisions.push(makePlannerDecision(op, choice));
|
|
434
|
+
let changedPaths;
|
|
435
|
+
if (choice.strategy === 'dirty-diff') {
|
|
436
|
+
working = emitDirtyPathDiffMutation(working, op.path, op, choice, patch, lowered, dirtyPaths, planner);
|
|
437
|
+
changedPaths = mutationDirtyPaths([], op.path, op);
|
|
438
|
+
}
|
|
439
|
+
else {
|
|
440
|
+
const patchStart = patch.length;
|
|
441
|
+
changedPaths = emitAbsoluteMutation(working, [], op.path, op, patch, lowered, warnings);
|
|
442
|
+
working = applyPatchOperationsToWorking(working, patch, patchStart);
|
|
443
|
+
for (const dirtyPath of changedPaths)
|
|
444
|
+
dirtyPaths.push(dirtyPath);
|
|
445
|
+
}
|
|
446
|
+
for (const dirtyPath of changedPaths)
|
|
447
|
+
noteAbsoluteMutation(runtime, dirtyPath);
|
|
173
448
|
}
|
|
174
449
|
flushPendingRows(pendingRows, patch, dirtyRows);
|
|
175
450
|
const outputPatch = options.compact === false ? patch : compactMutationPatch(patch);
|
|
176
|
-
return { patch: outputPatch, matched, lowered, dirtyPaths, dirtyRows, warnings, matches: matchesOut };
|
|
451
|
+
return { patch: outputPatch, matched, lowered, dirtyPaths, dirtyRows, warnings, matches: matchesOut, decisions };
|
|
452
|
+
}
|
|
453
|
+
function compileMaterializedDiffMutationPlan(operations, source, options, planner, runtime) {
|
|
454
|
+
let working = cloneJson(source);
|
|
455
|
+
const lowered = [];
|
|
456
|
+
const dirtyPaths = [];
|
|
457
|
+
const dirtyRows = [];
|
|
458
|
+
const warnings = [];
|
|
459
|
+
const matchesOut = [];
|
|
460
|
+
const decisions = [];
|
|
461
|
+
let matched = 0;
|
|
462
|
+
for (const op of operations) {
|
|
463
|
+
if (op.repeat === 0 || isNoopRepeat(op)) {
|
|
464
|
+
lowered.push(op.kind + '-repeat-noop');
|
|
465
|
+
decisions.push(makePlannerDecision(op, { strategy: 'noop', reason: 'repeat-noop' }));
|
|
466
|
+
continue;
|
|
467
|
+
}
|
|
468
|
+
if (op.selector !== undefined) {
|
|
469
|
+
const context = resolveSelector(working, op.selector, runtime);
|
|
470
|
+
matched += context.matches.length * op.matchWeight;
|
|
471
|
+
for (const match of context.matches)
|
|
472
|
+
matchesOut.push(makeSelectorMatchOut(match, context.selector));
|
|
473
|
+
if (context.matches.length === 0) {
|
|
474
|
+
lowered.push(op.kind + '-selector-empty');
|
|
475
|
+
decisions.push(makePlannerDecision(op, { strategy: 'noop', reason: 'selector-empty' }, context));
|
|
476
|
+
continue;
|
|
477
|
+
}
|
|
478
|
+
const choice = {
|
|
479
|
+
strategy: 'materialize-diff',
|
|
480
|
+
reason: 'forced-materialize-diff'
|
|
481
|
+
};
|
|
482
|
+
decisions.push(makePlannerDecision(op, choice, context));
|
|
483
|
+
for (const match of context.matches) {
|
|
484
|
+
const absolutePath = match.path.concat(op.path);
|
|
485
|
+
working = applyMaterializedOperationToWorking(working, match.path, absolutePath, op);
|
|
486
|
+
for (const dirtyPath of mutationDirtyPaths(match.path, absolutePath, op)) {
|
|
487
|
+
dirtyPaths.push(dirtyPath);
|
|
488
|
+
noteAbsoluteMutation(runtime, dirtyPath);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
lowered.push(op.kind + '-selector-materialized');
|
|
492
|
+
continue;
|
|
493
|
+
}
|
|
494
|
+
decisions.push(makePlannerDecision(op, {
|
|
495
|
+
strategy: 'materialize-diff',
|
|
496
|
+
reason: 'forced-materialize-diff'
|
|
497
|
+
}));
|
|
498
|
+
working = applyMaterializedOperationToWorking(working, [], op.path, op);
|
|
499
|
+
for (const dirtyPath of mutationDirtyPaths([], op.path, op)) {
|
|
500
|
+
dirtyPaths.push(dirtyPath);
|
|
501
|
+
noteAbsoluteMutation(runtime, dirtyPath);
|
|
502
|
+
}
|
|
503
|
+
lowered.push(op.kind + '-materialized');
|
|
504
|
+
}
|
|
505
|
+
const patch = diffWithPlanner(planner, source, working, planner.diff);
|
|
506
|
+
const outputPatch = options.compact === false ? patch : compactMutationPatch(patch);
|
|
507
|
+
return { patch: outputPatch, matched, lowered, dirtyPaths, dirtyRows, warnings, matches: matchesOut, decisions };
|
|
177
508
|
}
|
|
178
509
|
export function commitMutation(state, plan, options) {
|
|
179
510
|
const result = compileMutationPlan(plan, state.get(), options);
|
|
@@ -182,42 +513,513 @@ export function commitMutation(state, plan, options) {
|
|
|
182
513
|
}
|
|
183
514
|
export function commitCrdtMutation(doc, plan, options) {
|
|
184
515
|
const state = doc.toJSON();
|
|
516
|
+
const planner = normalizePlannerOptions(options);
|
|
185
517
|
const compiled = compileMutationPlan(plan, state, options);
|
|
186
|
-
const operations =
|
|
518
|
+
const operations = optimizeMutationOperations(plan.operations);
|
|
519
|
+
const crdtDecisions = [];
|
|
520
|
+
const changeOptions = planner.crdtMetadata === undefined
|
|
521
|
+
? undefined
|
|
522
|
+
: { metadata: cloneJson(planner.crdtMetadata) };
|
|
523
|
+
const runtime = createMutationRuntime(planner.schema);
|
|
524
|
+
const nativeSequenceBackings = collectNativeCrdtSequenceBackings(doc);
|
|
187
525
|
let commit;
|
|
188
526
|
if (operations.length === 0) {
|
|
189
|
-
commit = doc.change(() => { });
|
|
527
|
+
commit = doc.change(() => { }, changeOptions);
|
|
190
528
|
}
|
|
191
529
|
else {
|
|
192
530
|
let working = cloneJson(state);
|
|
531
|
+
const materializedWrites = [];
|
|
532
|
+
const nativeSequenceWrites = [];
|
|
193
533
|
commit = doc.change((tx) => {
|
|
194
534
|
for (const op of operations) {
|
|
195
|
-
if (op.repeat === 0 || isNoopRepeat(op))
|
|
535
|
+
if (op.repeat === 0 || isNoopRepeat(op)) {
|
|
536
|
+
crdtDecisions.push(makeCrdtPlannerDecision(op, op.path, {
|
|
537
|
+
strategy: 'noop',
|
|
538
|
+
reason: 'repeat-noop'
|
|
539
|
+
}));
|
|
196
540
|
continue;
|
|
541
|
+
}
|
|
197
542
|
if (op.selector === undefined) {
|
|
198
|
-
|
|
199
|
-
|
|
543
|
+
const decision = chooseCrdtStrategy(planner, working, op, op.path, undefined, operations.length, materializedWrites, nativeSequenceWrites, nativeSequenceBackings);
|
|
544
|
+
crdtDecisions.push(decision);
|
|
545
|
+
applyCrdtOperation(tx, working, [], op.path, op, decision);
|
|
546
|
+
trackCrdtWrite(materializedWrites, nativeSequenceWrites, op.path, op, decision);
|
|
547
|
+
working = applyMaterializedOperationToWorking(working, [], op.path, op);
|
|
548
|
+
noteAbsoluteMutation(runtime, op.path);
|
|
549
|
+
continue;
|
|
550
|
+
}
|
|
551
|
+
const context = resolveSelector(working, op.selector, runtime);
|
|
552
|
+
if (context.matches.length === 0) {
|
|
553
|
+
crdtDecisions.push(makeCrdtPlannerDecision(op, op.path, {
|
|
554
|
+
strategy: 'noop',
|
|
555
|
+
reason: 'selector-empty'
|
|
556
|
+
}, op.selector.path));
|
|
200
557
|
continue;
|
|
201
558
|
}
|
|
202
|
-
const context = resolveSelector(working, op.selector);
|
|
203
559
|
for (const match of context.matches) {
|
|
204
560
|
const absolutePath = match.path.concat(op.path);
|
|
205
|
-
|
|
206
|
-
|
|
561
|
+
const decision = chooseCrdtStrategy(planner, working, op, absolutePath, op.selector.path, operations.length, materializedWrites, nativeSequenceWrites, nativeSequenceBackings);
|
|
562
|
+
crdtDecisions.push(decision);
|
|
563
|
+
applyCrdtOperation(tx, working, match.path, absolutePath, op, decision);
|
|
564
|
+
trackCrdtWrite(materializedWrites, nativeSequenceWrites, absolutePath, op, decision);
|
|
565
|
+
working = applyMaterializedOperationToWorking(working, match.path, absolutePath, op);
|
|
566
|
+
noteAbsoluteMutation(runtime, absolutePath);
|
|
207
567
|
}
|
|
208
568
|
}
|
|
209
|
-
});
|
|
569
|
+
}, changeOptions);
|
|
570
|
+
}
|
|
571
|
+
return { ...compiled, commit, crdtDecisions };
|
|
572
|
+
}
|
|
573
|
+
function normalizePlannerOptions(options = {}) {
|
|
574
|
+
return {
|
|
575
|
+
strategy: normalizePatchStrategy(options.strategy ?? options.planner?.strategy ?? 'auto'),
|
|
576
|
+
crdt: normalizeCrdtStrategy(options.planner?.crdt ?? 'auto'),
|
|
577
|
+
crdtAssignmentPolicy: normalizeCrdtAssignmentPolicy(options.planner?.crdtAssignmentPolicy ?? 'preserve-conflicts'),
|
|
578
|
+
crdtMetadata: normalizeCrdtMetadata(options.planner?.crdtMetadata),
|
|
579
|
+
schema: normalizeMutationSchema(options.planner?.schema),
|
|
580
|
+
diff: {
|
|
581
|
+
...(options.diff || {}),
|
|
582
|
+
...(options.planner?.diff || {})
|
|
583
|
+
},
|
|
584
|
+
diffEngine: options.planner?.diffEngine,
|
|
585
|
+
dirtyDiffMinSelectivity: normalizeSelectivityThreshold(options.planner?.dirtyDiffMinSelectivity)
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function normalizePatchStrategy(strategy) {
|
|
589
|
+
if (strategy === 'auto' ||
|
|
590
|
+
strategy === 'direct' ||
|
|
591
|
+
strategy === 'row-field' ||
|
|
592
|
+
strategy === 'dirty-diff' ||
|
|
593
|
+
strategy === 'materialize-diff') {
|
|
594
|
+
return strategy;
|
|
595
|
+
}
|
|
596
|
+
throw new TypeError('unsupported mutation patch strategy: ' + strategy);
|
|
597
|
+
}
|
|
598
|
+
function normalizeCrdtStrategy(strategy) {
|
|
599
|
+
if (strategy === 'auto' || strategy === 'native' || strategy === 'materialize')
|
|
600
|
+
return strategy;
|
|
601
|
+
throw new TypeError('unsupported mutation CRDT strategy: ' + strategy);
|
|
602
|
+
}
|
|
603
|
+
function normalizeCrdtAssignmentPolicy(policy) {
|
|
604
|
+
if (policy === 'preserve-conflicts' || policy === 'last-write-wins' || policy === 'materialize')
|
|
605
|
+
return policy;
|
|
606
|
+
throw new TypeError('unsupported mutation CRDT assignment policy: ' + policy);
|
|
607
|
+
}
|
|
608
|
+
function normalizeCrdtMetadata(metadata) {
|
|
609
|
+
if (metadata === undefined)
|
|
610
|
+
return undefined;
|
|
611
|
+
if (!isObjectRecord(metadata))
|
|
612
|
+
throw new TypeError('mutation CRDT metadata must be a JSON object');
|
|
613
|
+
return cloneJson(metadata);
|
|
614
|
+
}
|
|
615
|
+
function normalizeMutationSchema(schema) {
|
|
616
|
+
if (schema === undefined)
|
|
617
|
+
return { tables: [] };
|
|
618
|
+
const entries = Array.isArray(schema)
|
|
619
|
+
? schema
|
|
620
|
+
: (schema.tables || []).concat(schema.entities || []);
|
|
621
|
+
const tables = [];
|
|
622
|
+
for (const table of entries) {
|
|
623
|
+
const normalized = {
|
|
624
|
+
path: normalizePath(table.path, 'mutation schema table path'),
|
|
625
|
+
key: table.key === undefined ? undefined : normalizePath(table.key, 'mutation schema key'),
|
|
626
|
+
stableRowShape: table.stableRowShape !== false,
|
|
627
|
+
numericFields: normalizeSchemaFields(table.numericFields, 'mutation schema numericFields'),
|
|
628
|
+
textFields: normalizeSchemaFields(table.textFields, 'mutation schema textFields'),
|
|
629
|
+
listFields: normalizeSchemaFields(table.listFields, 'mutation schema listFields'),
|
|
630
|
+
selectorFields: normalizeSchemaFields(table.selectorFields, 'mutation schema selectorFields')
|
|
631
|
+
};
|
|
632
|
+
if (normalized.path.length === 0)
|
|
633
|
+
throw new TypeError('mutation schema table path must not be root');
|
|
634
|
+
tables.push(normalized);
|
|
635
|
+
}
|
|
636
|
+
return { tables };
|
|
637
|
+
}
|
|
638
|
+
function normalizeSchemaFields(fields, label) {
|
|
639
|
+
if (fields === undefined)
|
|
640
|
+
return [];
|
|
641
|
+
if (!Array.isArray(fields))
|
|
642
|
+
throw new TypeError(label + ' must be an array');
|
|
643
|
+
return fields.map((field) => normalizePath(field, label));
|
|
644
|
+
}
|
|
645
|
+
function normalizeSelectivityThreshold(value) {
|
|
646
|
+
if (value === undefined)
|
|
647
|
+
return DEFAULT_DIRTY_DIFF_MIN_SELECTIVITY;
|
|
648
|
+
if (!Number.isFinite(value))
|
|
649
|
+
throw new TypeError('dirtyDiffMinSelectivity must be a finite number');
|
|
650
|
+
if (value < 0)
|
|
651
|
+
return 0;
|
|
652
|
+
if (value > 1)
|
|
653
|
+
return 1;
|
|
654
|
+
return value;
|
|
655
|
+
}
|
|
656
|
+
function diffWithPlanner(planner, source, target, options) {
|
|
657
|
+
return planner.diffEngine === undefined
|
|
658
|
+
? diff(source, target, options)
|
|
659
|
+
: planner.diffEngine.diff(source, target, options);
|
|
660
|
+
}
|
|
661
|
+
function chooseSelectorStrategy(planner, op, context) {
|
|
662
|
+
const rowFieldEligible = canUseRowFieldMutation(context, op);
|
|
663
|
+
const dirtyRows = rowFieldEligible ? [makeDirtyRowsFrontier(context, op)] : undefined;
|
|
664
|
+
const dirtyPaths = rowFieldEligible ? undefined : makeSelectorDirtyPaths(context, op);
|
|
665
|
+
if (planner.strategy === 'direct') {
|
|
666
|
+
return { strategy: 'direct', reason: 'forced-direct' };
|
|
667
|
+
}
|
|
668
|
+
if (planner.strategy === 'row-field') {
|
|
669
|
+
return rowFieldEligible
|
|
670
|
+
? { strategy: 'row-field', reason: 'forced-row-field' }
|
|
671
|
+
: { strategy: 'direct', reason: 'row-field-ineligible-direct' };
|
|
672
|
+
}
|
|
673
|
+
if (planner.strategy === 'dirty-diff') {
|
|
674
|
+
return rowFieldEligible
|
|
675
|
+
? { strategy: 'dirty-diff', reason: 'forced-dirty-row-diff', dirtyRows }
|
|
676
|
+
: { strategy: 'dirty-diff', reason: 'forced-dirty-path-diff', dirtyPaths };
|
|
677
|
+
}
|
|
678
|
+
if (planner.strategy === 'materialize-diff') {
|
|
679
|
+
return { strategy: 'materialize-diff', reason: 'forced-materialize-diff' };
|
|
680
|
+
}
|
|
681
|
+
const selectivity = selectorSelectivity(context);
|
|
682
|
+
if (selectivity !== null && selectivity >= planner.dirtyDiffMinSelectivity) {
|
|
683
|
+
return rowFieldEligible
|
|
684
|
+
? { strategy: 'dirty-diff', reason: 'auto-dirty-row-diff-dense-selector', dirtyRows }
|
|
685
|
+
: { strategy: 'dirty-diff', reason: 'auto-dirty-path-diff-dense-selector', dirtyPaths };
|
|
686
|
+
}
|
|
687
|
+
if (rowFieldEligible)
|
|
688
|
+
return { strategy: 'row-field', reason: 'auto-row-field-sparse-selector' };
|
|
689
|
+
return { strategy: 'direct', reason: 'auto-direct-selector' };
|
|
690
|
+
}
|
|
691
|
+
function chooseAbsoluteStrategy(planner, op) {
|
|
692
|
+
if (planner.strategy === 'dirty-diff') {
|
|
693
|
+
return { strategy: 'dirty-diff', reason: 'forced-dirty-path-diff', dirtyPaths: [op.path.slice()] };
|
|
694
|
+
}
|
|
695
|
+
if (planner.strategy === 'materialize-diff') {
|
|
696
|
+
return { strategy: 'materialize-diff', reason: 'forced-materialize-diff' };
|
|
697
|
+
}
|
|
698
|
+
return { strategy: 'direct', reason: planner.strategy === 'direct' ? 'forced-direct' : 'auto-direct-absolute' };
|
|
699
|
+
}
|
|
700
|
+
function makePlannerDecision(op, choice, context) {
|
|
701
|
+
const decision = {
|
|
702
|
+
strategy: choice.strategy,
|
|
703
|
+
reason: choice.reason,
|
|
704
|
+
kind: op.kind,
|
|
705
|
+
path: op.path.slice()
|
|
706
|
+
};
|
|
707
|
+
if (op.selector !== undefined)
|
|
708
|
+
decision.selectorPath = op.selector.path.slice();
|
|
709
|
+
if (context !== undefined) {
|
|
710
|
+
decision.matched = context.matches.length;
|
|
711
|
+
decision.collectionSize = context.collectionSize;
|
|
712
|
+
decision.selectivity = selectorSelectivity(context);
|
|
713
|
+
if (context.schema !== undefined)
|
|
714
|
+
decision.schema = makePlannerSchemaDecision(context, op);
|
|
715
|
+
}
|
|
716
|
+
if (choice.dirtyPaths !== undefined)
|
|
717
|
+
decision.dirtyPaths = clonePathList(choice.dirtyPaths);
|
|
718
|
+
if (choice.dirtyRows !== undefined)
|
|
719
|
+
decision.dirtyRows = cloneDirtyRowsFrontiers(choice.dirtyRows);
|
|
720
|
+
return decision;
|
|
721
|
+
}
|
|
722
|
+
function makePlannerSchemaDecision(context, op) {
|
|
723
|
+
const field = context.tailPath.concat(op.path);
|
|
724
|
+
const schema = context.schema;
|
|
725
|
+
const decision = {
|
|
726
|
+
tablePath: schema.path.slice(),
|
|
727
|
+
key: schema.key === undefined ? undefined : schema.key.slice(),
|
|
728
|
+
stableRowShape: schema.stableRowShape
|
|
729
|
+
};
|
|
730
|
+
const kind = schemaFieldKind(schema, field);
|
|
731
|
+
if (kind !== undefined)
|
|
732
|
+
decision.fieldKind = kind;
|
|
733
|
+
if (context.usedCache === true)
|
|
734
|
+
decision.selectorCached = true;
|
|
735
|
+
if (context.usedIndex === true)
|
|
736
|
+
decision.selectorIndexed = true;
|
|
737
|
+
return decision;
|
|
738
|
+
}
|
|
739
|
+
function canUseRowFieldMutation(context, op) {
|
|
740
|
+
return context.wildcardCount === 1 &&
|
|
741
|
+
Array.isArray(context.collection) &&
|
|
742
|
+
canUseRowFieldAssign(op) &&
|
|
743
|
+
context.matches.every((match) => match.rowIndex !== undefined);
|
|
744
|
+
}
|
|
745
|
+
function makeDirtyRowsFrontier(context, op) {
|
|
746
|
+
const rows = [];
|
|
747
|
+
for (const match of context.matches) {
|
|
748
|
+
if (match.rowIndex !== undefined)
|
|
749
|
+
rows.push(match.rowIndex);
|
|
750
|
+
}
|
|
751
|
+
return {
|
|
752
|
+
path: context.basePath.slice(),
|
|
753
|
+
rows,
|
|
754
|
+
fields: [context.tailPath.concat(op.path)]
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
function makeSelectorDirtyPaths(context, op) {
|
|
758
|
+
const paths = [];
|
|
759
|
+
for (const match of context.matches)
|
|
760
|
+
paths.push(match.path.concat(op.path));
|
|
761
|
+
return paths;
|
|
762
|
+
}
|
|
763
|
+
function collectionSizeOf(collection) {
|
|
764
|
+
if (Array.isArray(collection))
|
|
765
|
+
return collection.length;
|
|
766
|
+
if (isObjectRecord(collection))
|
|
767
|
+
return Object.keys(collection).length;
|
|
768
|
+
return 0;
|
|
769
|
+
}
|
|
770
|
+
function selectorSelectivity(context) {
|
|
771
|
+
const size = context.collectionSize;
|
|
772
|
+
return size === 0 ? null : context.matches.length / size;
|
|
773
|
+
}
|
|
774
|
+
function clonePathList(paths) {
|
|
775
|
+
return paths.map((path) => path.slice());
|
|
776
|
+
}
|
|
777
|
+
function cloneDirtyRowsFrontiers(frontiers) {
|
|
778
|
+
return frontiers.map((frontier) => ({
|
|
779
|
+
path: frontier.path.slice(),
|
|
780
|
+
rows: frontier.rows.slice(),
|
|
781
|
+
fields: frontier.fields === undefined ? undefined : frontier.fields.map((field) => field.slice())
|
|
782
|
+
}));
|
|
783
|
+
}
|
|
784
|
+
function toDirtyRowsResult(frontier) {
|
|
785
|
+
return {
|
|
786
|
+
path: frontier.path.slice(),
|
|
787
|
+
rows: frontier.rows.slice(),
|
|
788
|
+
fields: frontier.fields === undefined ? [] : frontier.fields.map((field) => field.slice())
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function emitDirtySelectorDiffMutation(source, context, op, choice, patch, lowered, dirtyPaths, dirtyRows, planner) {
|
|
792
|
+
let target = cloneJson(source);
|
|
793
|
+
const operationDirtyPaths = [];
|
|
794
|
+
for (const match of context.matches) {
|
|
795
|
+
const absolutePath = match.path.concat(op.path);
|
|
796
|
+
target = applyMaterializedOperationToWorking(target, match.path, absolutePath, op);
|
|
797
|
+
for (const dirtyPath of mutationDirtyPaths(match.path, absolutePath, op)) {
|
|
798
|
+
operationDirtyPaths.push(dirtyPath);
|
|
799
|
+
dirtyPaths.push(dirtyPath);
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
if (choice.dirtyRows !== undefined) {
|
|
803
|
+
patch.push(...diffWithPlanner(planner, source, target, {
|
|
804
|
+
...planner.diff,
|
|
805
|
+
dirtyPaths: undefined,
|
|
806
|
+
dirtyRows: choice.dirtyRows
|
|
807
|
+
}));
|
|
808
|
+
for (const frontier of choice.dirtyRows)
|
|
809
|
+
dirtyRows.push(toDirtyRowsResult(frontier));
|
|
810
|
+
lowered.push(op.kind + '-selector-dirty-row-diff');
|
|
210
811
|
}
|
|
211
|
-
|
|
812
|
+
else {
|
|
813
|
+
patch.push(...diffWithPlanner(planner, source, target, {
|
|
814
|
+
...planner.diff,
|
|
815
|
+
dirtyRows: undefined,
|
|
816
|
+
dirtyPaths: choice.dirtyPaths || operationDirtyPaths
|
|
817
|
+
}));
|
|
818
|
+
lowered.push(op.kind + '-selector-dirty-path-diff');
|
|
819
|
+
}
|
|
820
|
+
return target;
|
|
821
|
+
}
|
|
822
|
+
function emitDirtyPathDiffMutation(source, path, op, choice, patch, lowered, dirtyPaths, planner) {
|
|
823
|
+
const target = applyMaterializedOperationToWorking(cloneJson(source), [], path, op);
|
|
824
|
+
const operationDirtyPaths = choice.dirtyPaths || mutationDirtyPaths([], path, op);
|
|
825
|
+
patch.push(...diffWithPlanner(planner, source, target, {
|
|
826
|
+
...planner.diff,
|
|
827
|
+
dirtyRows: undefined,
|
|
828
|
+
dirtyPaths: operationDirtyPaths
|
|
829
|
+
}));
|
|
830
|
+
for (const dirtyPath of operationDirtyPaths)
|
|
831
|
+
dirtyPaths.push(dirtyPath.slice());
|
|
832
|
+
lowered.push(op.kind + '-dirty-path-diff');
|
|
833
|
+
return target;
|
|
834
|
+
}
|
|
835
|
+
function chooseCrdtStrategy(planner, state, op, path, selectorPath, operationCount, materializedWrites, nativeSequenceWrites, nativeSequenceBackings) {
|
|
836
|
+
if (op.kind === 'test') {
|
|
837
|
+
assertMutationExpected(readPath(state, path), op.expected, path);
|
|
838
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
839
|
+
strategy: 'noop',
|
|
840
|
+
reason: 'precondition-only'
|
|
841
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
842
|
+
}
|
|
843
|
+
if (op.kind === 'ensure' && readPath(state, path) !== undefined) {
|
|
844
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
845
|
+
strategy: 'noop',
|
|
846
|
+
reason: 'ensure-present'
|
|
847
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
848
|
+
}
|
|
849
|
+
if (op.kind === 'compareAndSet') {
|
|
850
|
+
assertMutationExpected(readPath(state, path), op.expected, path);
|
|
851
|
+
}
|
|
852
|
+
if (planner.crdt === 'materialize') {
|
|
853
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
854
|
+
strategy: 'materialized-set',
|
|
855
|
+
reason: 'forced-materialize'
|
|
856
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
857
|
+
}
|
|
858
|
+
if (isRemoveOperation(op) && path.length > 0 && !hasOverlappingPath(materializedWrites, path)) {
|
|
859
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
860
|
+
strategy: 'native-delete',
|
|
861
|
+
reason: planner.crdt === 'native' ? 'forced-native-delete' : 'auto-native-delete'
|
|
862
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
863
|
+
}
|
|
864
|
+
if (isArithmeticOperation(op)) {
|
|
865
|
+
if (canUseNativeCrdtCounter(path, operationCount, materializedWrites)) {
|
|
866
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
867
|
+
strategy: 'native-counter',
|
|
868
|
+
reason: planner.crdt === 'native' ? 'forced-native-counter' : 'auto-native-counter'
|
|
869
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
870
|
+
}
|
|
871
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
872
|
+
strategy: 'materialized-set',
|
|
873
|
+
reason: 'native-counter-ineligible-materialized'
|
|
874
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
875
|
+
}
|
|
876
|
+
if (isTextAppendOperation(op) || isTextSpliceOperation(op)) {
|
|
877
|
+
if (canUseNativeCrdtText(state, path, op, materializedWrites, nativeSequenceWrites, nativeSequenceBackings)) {
|
|
878
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
879
|
+
strategy: isTextSpliceOperation(op) ? 'native-text-splice' : 'native-text',
|
|
880
|
+
reason: planner.crdt === 'native'
|
|
881
|
+
? (isTextSpliceOperation(op) ? 'forced-native-text-splice' : 'forced-native-text')
|
|
882
|
+
: (isTextSpliceOperation(op) ? 'auto-native-text-splice' : 'auto-native-text')
|
|
883
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
884
|
+
}
|
|
885
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
886
|
+
strategy: 'materialized-set',
|
|
887
|
+
reason: 'native-text-ineligible-materialized'
|
|
888
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
889
|
+
}
|
|
890
|
+
if (isNativeListCandidateOperation(op)) {
|
|
891
|
+
if (canUseNativeCrdtList(state, path, materializedWrites, nativeSequenceWrites, nativeSequenceBackings)) {
|
|
892
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
893
|
+
strategy: 'native-list',
|
|
894
|
+
reason: planner.crdt === 'native' ? 'forced-native-list' : 'auto-native-list'
|
|
895
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
896
|
+
}
|
|
897
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
898
|
+
strategy: 'materialized-set',
|
|
899
|
+
reason: 'native-list-ineligible-materialized'
|
|
900
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
901
|
+
}
|
|
902
|
+
if (isMapFieldMutationOperation(op) && planner.crdtAssignmentPolicy !== 'preserve-conflicts') {
|
|
903
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
904
|
+
strategy: 'materialized-set',
|
|
905
|
+
reason: 'crdt-assignment-policy-materialized'
|
|
906
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
907
|
+
}
|
|
908
|
+
if (canUseNativeCrdtMapField(state, path, op, planner.crdtAssignmentPolicy, materializedWrites, nativeSequenceWrites)) {
|
|
909
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
910
|
+
strategy: 'native-map-field',
|
|
911
|
+
reason: planner.crdt === 'native' ? 'forced-native-map-field' : 'auto-native-map-field'
|
|
912
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
913
|
+
}
|
|
914
|
+
return makeCrdtPlannerDecision(op, path, {
|
|
915
|
+
strategy: 'materialized-set',
|
|
916
|
+
reason: planner.crdt === 'native' ? 'native-unsupported-materialized' : 'auto-materialized'
|
|
917
|
+
}, selectorPath, planner.crdtAssignmentPolicy);
|
|
918
|
+
}
|
|
919
|
+
function makeCrdtPlannerDecision(op, path, choice, selectorPath, assignmentPolicy) {
|
|
920
|
+
const native = isNativeCrdtDecisionStrategy(choice.strategy);
|
|
921
|
+
const decision = {
|
|
922
|
+
strategy: choice.strategy,
|
|
923
|
+
reason: choice.reason,
|
|
924
|
+
kind: op.kind,
|
|
925
|
+
path: path.slice(),
|
|
926
|
+
native
|
|
927
|
+
};
|
|
928
|
+
if (selectorPath !== undefined)
|
|
929
|
+
decision.selectorPath = selectorPath.slice();
|
|
930
|
+
if (assignmentPolicy !== undefined && isAssignmentLikeOperation(op))
|
|
931
|
+
decision.assignmentPolicy = assignmentPolicy;
|
|
932
|
+
decision.metadata = makeCrdtDecisionMetadata(decision, op);
|
|
933
|
+
return decision;
|
|
934
|
+
}
|
|
935
|
+
function isNativeCrdtDecisionStrategy(strategy) {
|
|
936
|
+
return strategy === 'native-counter' ||
|
|
937
|
+
strategy === 'native-text' ||
|
|
938
|
+
strategy === 'native-text-splice' ||
|
|
939
|
+
strategy === 'native-list' ||
|
|
940
|
+
strategy === 'native-map-field' ||
|
|
941
|
+
strategy === 'native-delete';
|
|
942
|
+
}
|
|
943
|
+
function makeCrdtDecisionMetadata(decision, op) {
|
|
944
|
+
const metadata = {
|
|
945
|
+
strategy: decision.strategy,
|
|
946
|
+
reason: decision.reason,
|
|
947
|
+
kind: decision.kind,
|
|
948
|
+
path: decision.path.slice(),
|
|
949
|
+
native: decision.native,
|
|
950
|
+
repeat: op.repeat
|
|
951
|
+
};
|
|
952
|
+
if (decision.selectorPath !== undefined)
|
|
953
|
+
metadata.selectorPath = decision.selectorPath.slice();
|
|
954
|
+
if (decision.assignmentPolicy !== undefined)
|
|
955
|
+
metadata.assignmentPolicy = decision.assignmentPolicy;
|
|
956
|
+
if (isArithmeticOperation(op))
|
|
957
|
+
metadata.delta = signedArithmeticDelta(op);
|
|
958
|
+
if (op.kind === 'multiply')
|
|
959
|
+
metadata.factor = op.delta;
|
|
960
|
+
if (op.kind === 'min' || op.kind === 'max')
|
|
961
|
+
metadata.value = op.delta;
|
|
962
|
+
if (op.kind === 'clamp') {
|
|
963
|
+
metadata.min = op.min;
|
|
964
|
+
metadata.max = op.max;
|
|
965
|
+
}
|
|
966
|
+
if (isListInsertOperation(op) || op.kind === 'append' || op.kind === 'addToSet')
|
|
967
|
+
metadata.valueCount = (op.values || []).length * op.repeat;
|
|
968
|
+
if (isListSpliceOperation(op)) {
|
|
969
|
+
metadata.start = op.start || 0;
|
|
970
|
+
metadata.deleteCount = op.deleteCount || 0;
|
|
971
|
+
metadata.valueCount = (op.values || []).length * op.repeat;
|
|
972
|
+
}
|
|
973
|
+
if (op.kind === 'moveItem') {
|
|
974
|
+
metadata.fromIndex = op.start || 0;
|
|
975
|
+
metadata.toIndex = op.toIndex || 0;
|
|
976
|
+
metadata.count = op.deleteCount || 1;
|
|
977
|
+
}
|
|
978
|
+
if (isTextAppendOperation(op))
|
|
979
|
+
metadata.textLength = Array.from((op.text || '').repeat(op.repeat)).length;
|
|
980
|
+
if (isTextSpliceOperation(op)) {
|
|
981
|
+
metadata.start = op.start || 0;
|
|
982
|
+
metadata.deleteCount = op.deleteCount || 0;
|
|
983
|
+
metadata.textLength = Array.from(op.text || '').length * op.repeat;
|
|
984
|
+
}
|
|
985
|
+
if (op.kind === 'formatText') {
|
|
986
|
+
metadata.start = op.start || 0;
|
|
987
|
+
metadata.length = op.length || 0;
|
|
988
|
+
metadata.attributes = cloneJson(op.attributes || {});
|
|
989
|
+
}
|
|
990
|
+
if (op.kind === 'assign' && isObjectRecord(op.value))
|
|
991
|
+
metadata.fields = Object.keys(op.value);
|
|
992
|
+
if (op.transaction !== undefined)
|
|
993
|
+
metadata.transaction = cloneTransactionInfo(op.transaction);
|
|
994
|
+
return metadata;
|
|
212
995
|
}
|
|
213
996
|
function applyPatchOperationsToWorking(working, patch, start) {
|
|
214
997
|
if (patch.length === start)
|
|
215
998
|
return working;
|
|
216
|
-
return applyPatch(working, patch.slice(start));
|
|
999
|
+
return applyPatch(working, patch.slice(start), { cloneValues: true });
|
|
217
1000
|
}
|
|
218
|
-
function applyMaterializedOperationToWorking(working, path, op) {
|
|
219
|
-
|
|
220
|
-
|
|
1001
|
+
function applyMaterializedOperationToWorking(working, basePath, path, op) {
|
|
1002
|
+
if (op.kind === 'test') {
|
|
1003
|
+
assertMutationExpected(readPath(working, path), op.expected, path);
|
|
1004
|
+
return working;
|
|
1005
|
+
}
|
|
1006
|
+
if (op.kind === 'copy') {
|
|
1007
|
+
return setPathInWorking(working, resolveOperationToPath(basePath, op), cloneJson(readRequiredPath(working, path)));
|
|
1008
|
+
}
|
|
1009
|
+
if (op.kind === 'move') {
|
|
1010
|
+
return movePathInWorking(working, path, resolveOperationToPath(basePath, op));
|
|
1011
|
+
}
|
|
1012
|
+
if (op.kind === 'rename') {
|
|
1013
|
+
return movePathInWorking(working, path, renameTargetPath(path, op));
|
|
1014
|
+
}
|
|
1015
|
+
if (isRemoveOperation(op)) {
|
|
1016
|
+
return removePathFromWorking(working, path);
|
|
1017
|
+
}
|
|
1018
|
+
const current = readPath(working, path);
|
|
1019
|
+
const nextValue = applyFieldMutation(current, op, path);
|
|
1020
|
+
if (op.kind === 'ensure' && current !== undefined)
|
|
1021
|
+
return working;
|
|
1022
|
+
return setPathInWorking(working, path, nextValue);
|
|
221
1023
|
}
|
|
222
1024
|
function compactMutationPatch(patch) {
|
|
223
1025
|
const compacted = [];
|
|
@@ -249,30 +1051,54 @@ function mergePatchOperation(previous, next) {
|
|
|
249
1051
|
}
|
|
250
1052
|
return false;
|
|
251
1053
|
}
|
|
252
|
-
function
|
|
253
|
-
const
|
|
1054
|
+
function optimizeMutationOperations(operations) {
|
|
1055
|
+
const optimized = [];
|
|
254
1056
|
for (const rawOp of operations) {
|
|
255
1057
|
const op = normalizeOperation(rawOp);
|
|
256
1058
|
if (op.repeat === 0 || isNoopRepeat(op))
|
|
257
1059
|
continue;
|
|
258
|
-
const
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
1060
|
+
const optimizedOp = withMatchWeight(op, 1);
|
|
1061
|
+
const previous = optimized[optimized.length - 1];
|
|
1062
|
+
if (previous !== undefined) {
|
|
1063
|
+
const merged = mergeOptimizedOperations(previous, optimizedOp);
|
|
1064
|
+
if (merged !== null) {
|
|
1065
|
+
optimized.pop();
|
|
1066
|
+
if (merged !== undefined)
|
|
1067
|
+
optimized.push(merged);
|
|
1068
|
+
continue;
|
|
1069
|
+
}
|
|
266
1070
|
}
|
|
267
|
-
|
|
1071
|
+
optimized.push(optimizedOp);
|
|
268
1072
|
}
|
|
269
|
-
return
|
|
1073
|
+
return optimized;
|
|
1074
|
+
}
|
|
1075
|
+
function withMatchWeight(op, matchWeight) {
|
|
1076
|
+
return { ...op, matchWeight };
|
|
270
1077
|
}
|
|
271
|
-
function
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
1078
|
+
function mergeOptimizedOperations(left, right) {
|
|
1079
|
+
if (!canMergeOptimizedOperations(left, right))
|
|
1080
|
+
return null;
|
|
1081
|
+
if (isArithmeticOperation(left) && isArithmeticOperation(right))
|
|
1082
|
+
return mergeArithmeticOperations(left, right);
|
|
1083
|
+
if (left.kind === 'append' && right.kind === 'append')
|
|
1084
|
+
return mergeAppendOperations(left, right);
|
|
1085
|
+
if (isTextAppendOperation(left) && isTextAppendOperation(right))
|
|
1086
|
+
return mergeAppendTextOperations(left, right);
|
|
1087
|
+
if (left.kind === 'toggle' && right.kind === 'toggle')
|
|
1088
|
+
return mergeToggleOperations(left, right);
|
|
1089
|
+
return null;
|
|
1090
|
+
}
|
|
1091
|
+
function canMergeOptimizedOperations(left, right) {
|
|
1092
|
+
if (!samePath(left.path, right.path) ||
|
|
1093
|
+
!sameOptionalPath(left.to, right.to) ||
|
|
1094
|
+
left.key !== right.key ||
|
|
1095
|
+
!sameSelectorPlan(left.selector, right.selector) ||
|
|
1096
|
+
!sameTransactionInfo(left.transaction, right.transaction))
|
|
1097
|
+
return false;
|
|
1098
|
+
if (left.selector === undefined)
|
|
1099
|
+
return true;
|
|
1100
|
+
return selectorMutationPreservesMembership(left.selector, left.path) &&
|
|
1101
|
+
selectorMutationPreservesMembership(left.selector, right.path);
|
|
276
1102
|
}
|
|
277
1103
|
function mergeArithmeticOperations(left, right) {
|
|
278
1104
|
const total = signedArithmeticDelta(left) + signedArithmeticDelta(right);
|
|
@@ -280,14 +1106,80 @@ function mergeArithmeticOperations(left, right) {
|
|
|
280
1106
|
if (total === 0)
|
|
281
1107
|
return undefined;
|
|
282
1108
|
return {
|
|
283
|
-
...left,
|
|
284
1109
|
kind: total < 0 ? 'decrement' : 'increment',
|
|
285
1110
|
delta: Math.abs(total),
|
|
286
1111
|
repeat: 1,
|
|
287
1112
|
path: left.path.slice(),
|
|
288
|
-
selector: left.selector === undefined ? undefined : cloneSelectorPlan(left.selector)
|
|
1113
|
+
selector: left.selector === undefined ? undefined : cloneSelectorPlan(left.selector),
|
|
1114
|
+
transaction: left.transaction === undefined ? undefined : cloneTransactionInfo(left.transaction),
|
|
1115
|
+
matchWeight: left.matchWeight + right.matchWeight
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function mergeAppendOperations(left, right) {
|
|
1119
|
+
return {
|
|
1120
|
+
kind: 'append',
|
|
1121
|
+
values: repeatValues(left.values || [], left.repeat).concat(repeatValues(right.values || [], right.repeat)),
|
|
1122
|
+
repeat: 1,
|
|
1123
|
+
path: left.path.slice(),
|
|
1124
|
+
selector: left.selector === undefined ? undefined : cloneSelectorPlan(left.selector),
|
|
1125
|
+
transaction: left.transaction === undefined ? undefined : cloneTransactionInfo(left.transaction),
|
|
1126
|
+
matchWeight: left.matchWeight + right.matchWeight
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
function mergeAppendTextOperations(left, right) {
|
|
1130
|
+
return {
|
|
1131
|
+
kind: 'appendText',
|
|
1132
|
+
text: (left.text || '').repeat(left.repeat) + (right.text || '').repeat(right.repeat),
|
|
1133
|
+
repeat: 1,
|
|
1134
|
+
path: left.path.slice(),
|
|
1135
|
+
selector: left.selector === undefined ? undefined : cloneSelectorPlan(left.selector),
|
|
1136
|
+
transaction: left.transaction === undefined ? undefined : cloneTransactionInfo(left.transaction),
|
|
1137
|
+
matchWeight: left.matchWeight + right.matchWeight
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
function mergeToggleOperations(left, right) {
|
|
1141
|
+
const repeat = (left.repeat + right.repeat) % 2;
|
|
1142
|
+
if (repeat === 0)
|
|
1143
|
+
return undefined;
|
|
1144
|
+
return {
|
|
1145
|
+
kind: 'toggle',
|
|
1146
|
+
repeat,
|
|
1147
|
+
path: left.path.slice(),
|
|
1148
|
+
selector: left.selector === undefined ? undefined : cloneSelectorPlan(left.selector),
|
|
1149
|
+
transaction: left.transaction === undefined ? undefined : cloneTransactionInfo(left.transaction),
|
|
1150
|
+
matchWeight: left.matchWeight + right.matchWeight
|
|
289
1151
|
};
|
|
290
1152
|
}
|
|
1153
|
+
function selectorMutationPreservesMembership(selector, path) {
|
|
1154
|
+
const fields = [];
|
|
1155
|
+
collectSelectorConditionFields(selector.conditions, fields);
|
|
1156
|
+
for (const field of fields) {
|
|
1157
|
+
if (overlappingMutationPaths(path, field))
|
|
1158
|
+
return false;
|
|
1159
|
+
}
|
|
1160
|
+
return true;
|
|
1161
|
+
}
|
|
1162
|
+
function collectSelectorConditionFields(conditions, out) {
|
|
1163
|
+
for (const condition of conditions)
|
|
1164
|
+
collectConditionFields(condition, out);
|
|
1165
|
+
}
|
|
1166
|
+
function collectConditionFields(condition, out) {
|
|
1167
|
+
if ('and' in condition) {
|
|
1168
|
+
collectSelectorConditionFields(condition.and, out);
|
|
1169
|
+
}
|
|
1170
|
+
else if ('or' in condition) {
|
|
1171
|
+
collectSelectorConditionFields(condition.or, out);
|
|
1172
|
+
}
|
|
1173
|
+
else if ('not' in condition) {
|
|
1174
|
+
collectConditionFields(condition.not, out);
|
|
1175
|
+
}
|
|
1176
|
+
else {
|
|
1177
|
+
out.push(normalizePath(condition.field, 'condition field'));
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
function overlappingMutationPaths(left, right) {
|
|
1181
|
+
return samePath(left, right) || isPathPrefix(left, right) || isPathPrefix(right, left);
|
|
1182
|
+
}
|
|
291
1183
|
function signedArithmeticDelta(op) {
|
|
292
1184
|
const sign = op.kind === 'decrement' ? -1 : 1;
|
|
293
1185
|
return sign * (op.delta || 0) * op.repeat;
|
|
@@ -295,12 +1187,72 @@ function signedArithmeticDelta(op) {
|
|
|
295
1187
|
function isArithmeticOperation(op) {
|
|
296
1188
|
return op.kind === 'increment' || op.kind === 'decrement';
|
|
297
1189
|
}
|
|
298
|
-
function
|
|
1190
|
+
function isAssignmentLikeOperation(op) {
|
|
1191
|
+
return op.kind === 'set' ||
|
|
1192
|
+
op.kind === 'ensure' ||
|
|
1193
|
+
op.kind === 'upsert' ||
|
|
1194
|
+
op.kind === 'assign' ||
|
|
1195
|
+
op.kind === 'toggle' ||
|
|
1196
|
+
op.kind === 'compareAndSet';
|
|
1197
|
+
}
|
|
1198
|
+
function isMapFieldMutationOperation(op) {
|
|
1199
|
+
return isAssignmentLikeOperation(op);
|
|
1200
|
+
}
|
|
1201
|
+
function emitAbsoluteMutation(source, basePath, path, op, patch, lowered, warnings) {
|
|
299
1202
|
const current = readPath(source, path);
|
|
1203
|
+
if (op.kind === 'test') {
|
|
1204
|
+
assertMutationExpected(current, op.expected, path);
|
|
1205
|
+
lowered.push('test');
|
|
1206
|
+
return [];
|
|
1207
|
+
}
|
|
1208
|
+
if (op.kind === 'copy') {
|
|
1209
|
+
const to = resolveOperationToPath(basePath, op);
|
|
1210
|
+
patch.push([OP_SET, to.slice(), cloneJson(readRequiredPath(source, path))]);
|
|
1211
|
+
lowered.push('copy-to-set');
|
|
1212
|
+
return [to];
|
|
1213
|
+
}
|
|
1214
|
+
if (op.kind === 'move') {
|
|
1215
|
+
const to = resolveOperationToPath(basePath, op);
|
|
1216
|
+
emitMovePatch(source, path, to, patch);
|
|
1217
|
+
lowered.push('move');
|
|
1218
|
+
return pathsEqualOrPair(path, to);
|
|
1219
|
+
}
|
|
1220
|
+
if (op.kind === 'rename') {
|
|
1221
|
+
const to = renameTargetPath(path, op);
|
|
1222
|
+
emitMovePatch(source, path, to, patch);
|
|
1223
|
+
lowered.push('rename-to-move');
|
|
1224
|
+
return pathsEqualOrPair(path, to);
|
|
1225
|
+
}
|
|
1226
|
+
if (isRemoveOperation(op)) {
|
|
1227
|
+
emitRemovePatch(source, path, patch);
|
|
1228
|
+
lowered.push(op.kind);
|
|
1229
|
+
return [path.slice()];
|
|
1230
|
+
}
|
|
300
1231
|
if (op.kind === 'set') {
|
|
301
1232
|
patch.push([OP_SET, path.slice(), cloneJson(op.value)]);
|
|
302
1233
|
lowered.push('set');
|
|
303
|
-
return;
|
|
1234
|
+
return [path.slice()];
|
|
1235
|
+
}
|
|
1236
|
+
if (op.kind === 'ensure') {
|
|
1237
|
+
if (current !== undefined) {
|
|
1238
|
+
lowered.push('ensure-noop');
|
|
1239
|
+
return [];
|
|
1240
|
+
}
|
|
1241
|
+
patch.push([OP_SET, path.slice(), readOperationValue(op, current, path)]);
|
|
1242
|
+
lowered.push('ensure-to-set');
|
|
1243
|
+
return [path.slice()];
|
|
1244
|
+
}
|
|
1245
|
+
if (op.kind === 'upsert') {
|
|
1246
|
+
const value = readOperationValue(op, current, path);
|
|
1247
|
+
if (isObjectRecord(current) && isObjectRecord(value)) {
|
|
1248
|
+
patch.push([OP_ASSIGN, path.slice(), cloneJson(value)]);
|
|
1249
|
+
lowered.push('upsert-to-assign');
|
|
1250
|
+
}
|
|
1251
|
+
else {
|
|
1252
|
+
patch.push([OP_SET, path.slice(), cloneJson(value)]);
|
|
1253
|
+
lowered.push('upsert-to-set');
|
|
1254
|
+
}
|
|
1255
|
+
return [path.slice()];
|
|
304
1256
|
}
|
|
305
1257
|
if (op.kind === 'assign') {
|
|
306
1258
|
const value = cloneJson(op.value);
|
|
@@ -311,22 +1263,34 @@ function emitAbsoluteMutation(source, path, op, patch, lowered, warnings) {
|
|
|
311
1263
|
patch.push([OP_SET, path.slice(), { ...value }]);
|
|
312
1264
|
}
|
|
313
1265
|
lowered.push('assign');
|
|
314
|
-
return;
|
|
1266
|
+
return [path.slice()];
|
|
1267
|
+
}
|
|
1268
|
+
if (op.kind === 'compareAndSet') {
|
|
1269
|
+
assertMutationExpected(current, op.expected, path);
|
|
1270
|
+
patch.push([OP_SET, path.slice(), readOperationValue(op, current, path)]);
|
|
1271
|
+
lowered.push('compareAndSet-to-set');
|
|
1272
|
+
return [path.slice()];
|
|
315
1273
|
}
|
|
316
1274
|
if (op.kind === 'increment' || op.kind === 'decrement') {
|
|
317
1275
|
patch.push([OP_SET, path.slice(), applyNumericMutation(current, op)]);
|
|
318
1276
|
lowered.push(op.kind + '-to-set');
|
|
319
|
-
return;
|
|
1277
|
+
return [path.slice()];
|
|
1278
|
+
}
|
|
1279
|
+
if (op.kind === 'multiply' || op.kind === 'min' || op.kind === 'max' || op.kind === 'clamp') {
|
|
1280
|
+
patch.push([OP_SET, path.slice(), applyAdvancedNumericMutation(current, op)]);
|
|
1281
|
+
lowered.push(op.kind + '-to-set');
|
|
1282
|
+
return [path.slice()];
|
|
320
1283
|
}
|
|
321
1284
|
if (op.kind === 'toggle') {
|
|
322
1285
|
if (op.repeat % 2 === 1) {
|
|
323
1286
|
patch.push([OP_SET, path.slice(), !Boolean(current)]);
|
|
324
1287
|
lowered.push('toggle-to-set');
|
|
1288
|
+
return [path.slice()];
|
|
325
1289
|
}
|
|
326
1290
|
else {
|
|
327
1291
|
lowered.push('toggle-repeat-noop');
|
|
1292
|
+
return [];
|
|
328
1293
|
}
|
|
329
|
-
return;
|
|
330
1294
|
}
|
|
331
1295
|
if (op.kind === 'append') {
|
|
332
1296
|
const values = repeatValues(op.values || [], op.repeat);
|
|
@@ -338,26 +1302,82 @@ function emitAbsoluteMutation(source, path, op, patch, lowered, warnings) {
|
|
|
338
1302
|
patch.push([OP_SET, path.slice(), values]);
|
|
339
1303
|
lowered.push('append-to-set');
|
|
340
1304
|
}
|
|
341
|
-
return;
|
|
1305
|
+
return [path.slice()];
|
|
342
1306
|
}
|
|
343
|
-
if (op.kind === '
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1307
|
+
if (op.kind === 'prepend' || op.kind === 'insert' || op.kind === 'removeAt' || op.kind === 'splice') {
|
|
1308
|
+
if (op.kind === 'splice' && op.repeat !== 1) {
|
|
1309
|
+
patch.push([OP_SET, path.slice(), applyListMutationToValue(current, op)]);
|
|
1310
|
+
lowered.push('splice-to-set');
|
|
1311
|
+
return [path.slice()];
|
|
1312
|
+
}
|
|
1313
|
+
const splice = listSpliceArgs(current, op);
|
|
1314
|
+
if (Array.isArray(current)) {
|
|
1315
|
+
patch.push([OP_ARRAY_SPLICE, path.slice(), splice.start, splice.deleteCount, splice.values]);
|
|
1316
|
+
lowered.push(op.kind + '-to-array-splice');
|
|
348
1317
|
}
|
|
349
1318
|
else {
|
|
350
|
-
patch.push([OP_SET, path.slice(),
|
|
351
|
-
lowered.push('
|
|
1319
|
+
patch.push([OP_SET, path.slice(), applyListMutationToValue(current, op)]);
|
|
1320
|
+
lowered.push(op.kind + '-to-set');
|
|
352
1321
|
}
|
|
353
|
-
return;
|
|
1322
|
+
return [path.slice()];
|
|
354
1323
|
}
|
|
355
|
-
if (op.kind === '
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
1324
|
+
if (op.kind === 'moveItem') {
|
|
1325
|
+
if (Array.isArray(current) && (op.deleteCount || 1) === 1) {
|
|
1326
|
+
patch.push([OP_ARRAY_MOVE, path.slice(), boundedIndex(op.start || 0, current.length), boundedIndex(op.toIndex || 0, current.length)]);
|
|
1327
|
+
lowered.push('moveItem-to-array-move');
|
|
1328
|
+
}
|
|
1329
|
+
else {
|
|
1330
|
+
patch.push([OP_SET, path.slice(), applyListMutationToValue(current, op)]);
|
|
1331
|
+
lowered.push('moveItem-to-set');
|
|
1332
|
+
}
|
|
1333
|
+
return [path.slice()];
|
|
360
1334
|
}
|
|
1335
|
+
if (op.kind === 'addToSet') {
|
|
1336
|
+
const values = addToSetValues(current, op);
|
|
1337
|
+
if (Array.isArray(current) && values.length !== 0) {
|
|
1338
|
+
patch.push([OP_APPEND, path.slice(), values]);
|
|
1339
|
+
lowered.push('addToSet-to-append');
|
|
1340
|
+
return [path.slice()];
|
|
1341
|
+
}
|
|
1342
|
+
if (Array.isArray(current)) {
|
|
1343
|
+
lowered.push('addToSet-noop');
|
|
1344
|
+
return [];
|
|
1345
|
+
}
|
|
1346
|
+
patch.push([OP_SET, path.slice(), values]);
|
|
1347
|
+
lowered.push('addToSet-to-set');
|
|
1348
|
+
return [path.slice()];
|
|
1349
|
+
}
|
|
1350
|
+
if (op.kind === 'pull' || op.kind === 'removeWhere') {
|
|
1351
|
+
patch.push([OP_SET, path.slice(), applyListMutationToValue(current, op)]);
|
|
1352
|
+
lowered.push(op.kind + '-to-set');
|
|
1353
|
+
return [path.slice()];
|
|
1354
|
+
}
|
|
1355
|
+
if (isTextAppendOperation(op) || isTextSpliceOperation(op)) {
|
|
1356
|
+
if (isTextSpliceOperation(op) && op.repeat !== 1) {
|
|
1357
|
+
patch.push([OP_SET, path.slice(), applyTextMutationToValue(current, op)]);
|
|
1358
|
+
lowered.push(op.kind + '-to-set');
|
|
1359
|
+
return [path.slice()];
|
|
1360
|
+
}
|
|
1361
|
+
const text = textInsertForOperation(op);
|
|
1362
|
+
const start = textStartForOperation(current, op);
|
|
1363
|
+
const deleteCount = textDeleteCountForOperation(current, op, start);
|
|
1364
|
+
if (typeof current === 'string') {
|
|
1365
|
+
patch.push([OP_STRING_SPLICE, path.slice(), start, deleteCount, text]);
|
|
1366
|
+
lowered.push(op.kind);
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
patch.push([OP_SET, path.slice(), applyTextMutationToValue(current, op)]);
|
|
1370
|
+
lowered.push(op.kind + '-to-set');
|
|
1371
|
+
}
|
|
1372
|
+
return [path.slice()];
|
|
1373
|
+
}
|
|
1374
|
+
if (op.kind === 'formatText') {
|
|
1375
|
+
patch.push([OP_SET, path.slice(), applyRichTextFormatMutation(current, op)]);
|
|
1376
|
+
lowered.push('formatText-to-set');
|
|
1377
|
+
return [path.slice()];
|
|
1378
|
+
}
|
|
1379
|
+
warnings.push('unsupported mutation kind: ' + op.kind);
|
|
1380
|
+
return [];
|
|
361
1381
|
}
|
|
362
1382
|
function queueRowFieldMutation(pending, context, op, working) {
|
|
363
1383
|
let entry = pending.find((item) => samePath(item.basePath, context.basePath) && samePath(item.tailPath, context.tailPath));
|
|
@@ -389,21 +1409,27 @@ function queueRowFieldMutation(pending, context, op, working) {
|
|
|
389
1409
|
values.length = entry.fields.length;
|
|
390
1410
|
const absolutePath = match.path.concat(op.path);
|
|
391
1411
|
const current = readPath(nextWorking, absolutePath);
|
|
392
|
-
const nextValue = applyFieldMutation(current, op);
|
|
1412
|
+
const nextValue = applyFieldMutation(current, op, absolutePath);
|
|
393
1413
|
values[fieldIndex] = nextValue;
|
|
394
1414
|
nextWorking = applyPatch(nextWorking, [[OP_SET, absolutePath, cloneJson(nextValue)]]);
|
|
395
1415
|
}
|
|
396
1416
|
return nextWorking;
|
|
397
1417
|
}
|
|
398
|
-
function hasIncompatiblePendingRows(pending, context) {
|
|
1418
|
+
function hasIncompatiblePendingRows(pending, context, field) {
|
|
399
1419
|
const rowIndexes = context.matches
|
|
400
1420
|
.map((match) => match.rowIndex)
|
|
401
1421
|
.filter((rowIndex) => rowIndex !== undefined);
|
|
402
1422
|
for (const entry of pending) {
|
|
1423
|
+
if (!sameSelectorPlan(entry.selector, context.selector))
|
|
1424
|
+
return true;
|
|
403
1425
|
if (!samePath(entry.basePath, context.basePath) || !samePath(entry.tailPath, context.tailPath))
|
|
404
1426
|
return true;
|
|
405
1427
|
if (!sameNumberArray(entry.rowIndexes, rowIndexes))
|
|
406
1428
|
return true;
|
|
1429
|
+
for (const existingField of entry.fields) {
|
|
1430
|
+
if (overlappingFieldPaths(existingField, field))
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
407
1433
|
}
|
|
408
1434
|
return false;
|
|
409
1435
|
}
|
|
@@ -421,55 +1447,328 @@ function flushPendingRows(pending, patch, dirtyRows) {
|
|
|
421
1447
|
}
|
|
422
1448
|
pending.length = 0;
|
|
423
1449
|
}
|
|
424
|
-
function applyFieldMutation(current, op) {
|
|
1450
|
+
function applyFieldMutation(current, op, path = []) {
|
|
425
1451
|
if (op.kind === 'set')
|
|
426
1452
|
return cloneJson(op.value);
|
|
1453
|
+
if (op.kind === 'ensure')
|
|
1454
|
+
return current === undefined ? readOperationValue(op, current, path) : current;
|
|
1455
|
+
if (op.kind === 'upsert')
|
|
1456
|
+
return applyUpsertMutation(current, op, path);
|
|
1457
|
+
if (op.kind === 'compareAndSet') {
|
|
1458
|
+
assertMutationExpected(current, op.expected, path);
|
|
1459
|
+
return readOperationValue(op, current, path);
|
|
1460
|
+
}
|
|
427
1461
|
if (op.kind === 'assign') {
|
|
428
1462
|
const value = cloneJson(op.value);
|
|
429
1463
|
return isObjectRecord(current) ? { ...current, ...value } : value;
|
|
430
1464
|
}
|
|
431
1465
|
if (op.kind === 'increment' || op.kind === 'decrement')
|
|
432
1466
|
return applyNumericMutation(current, op);
|
|
1467
|
+
if (op.kind === 'multiply' || op.kind === 'min' || op.kind === 'max' || op.kind === 'clamp')
|
|
1468
|
+
return applyAdvancedNumericMutation(current, op);
|
|
433
1469
|
if (op.kind === 'toggle')
|
|
434
1470
|
return op.repeat % 2 === 1 ? !Boolean(current) : current;
|
|
435
1471
|
if (op.kind === 'append')
|
|
436
1472
|
return Array.isArray(current) ? current.concat(repeatValues(op.values || [], op.repeat)) : repeatValues(op.values || [], op.repeat);
|
|
1473
|
+
if (op.kind === 'prepend')
|
|
1474
|
+
return repeatValues(op.values || [], op.repeat).concat(Array.isArray(current) ? current : []);
|
|
1475
|
+
if (op.kind === 'insert' || op.kind === 'removeAt' || op.kind === 'moveItem' || op.kind === 'addToSet' || op.kind === 'pull' || op.kind === 'removeWhere') {
|
|
1476
|
+
return applyListMutationToValue(current, op);
|
|
1477
|
+
}
|
|
437
1478
|
if (op.kind === 'appendText')
|
|
438
1479
|
return typeof current === 'string' ? current + (op.text || '').repeat(op.repeat) : (op.text || '').repeat(op.repeat);
|
|
1480
|
+
if (op.kind === 'insertText' || op.kind === 'deleteText' || op.kind === 'replaceText')
|
|
1481
|
+
return applyTextMutationToValue(current, op);
|
|
1482
|
+
if (op.kind === 'formatText')
|
|
1483
|
+
return applyRichTextFormatMutation(current, op);
|
|
439
1484
|
return applyMutationToValue(current, op, []);
|
|
440
1485
|
}
|
|
441
|
-
function applyCrdtOperation(tx, state, path, op) {
|
|
442
|
-
if (
|
|
443
|
-
|
|
1486
|
+
function applyCrdtOperation(tx, state, basePath, path, op, decision) {
|
|
1487
|
+
if (decision.strategy === 'noop')
|
|
1488
|
+
return;
|
|
1489
|
+
if (decision.strategy === 'native-counter') {
|
|
1490
|
+
if (op.kind === 'increment')
|
|
1491
|
+
tx.counter(path).increment((op.delta || 0) * op.repeat);
|
|
1492
|
+
else if (op.kind === 'decrement')
|
|
1493
|
+
tx.counter(path).decrement((op.delta || 0) * op.repeat);
|
|
1494
|
+
else
|
|
1495
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1496
|
+
return;
|
|
444
1497
|
}
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
1498
|
+
if (decision.strategy === 'native-text' || decision.strategy === 'native-text-splice') {
|
|
1499
|
+
applyNativeTextCrdtOperation(tx, state, path, op);
|
|
1500
|
+
return;
|
|
448
1501
|
}
|
|
449
|
-
|
|
450
|
-
tx
|
|
1502
|
+
if (decision.strategy === 'native-list') {
|
|
1503
|
+
applyNativeListCrdtOperation(tx, state, path, op);
|
|
1504
|
+
return;
|
|
451
1505
|
}
|
|
452
|
-
|
|
453
|
-
tx
|
|
1506
|
+
if (decision.strategy === 'native-map-field') {
|
|
1507
|
+
applyNativeMapFieldCrdtOperation(tx, state, path, op);
|
|
1508
|
+
return;
|
|
454
1509
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
1510
|
+
if (decision.strategy === 'native-delete') {
|
|
1511
|
+
tx.delete(path);
|
|
1512
|
+
return;
|
|
458
1513
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
1514
|
+
applyMaterializedCrdtOperation(tx, state, basePath, path, op);
|
|
1515
|
+
}
|
|
1516
|
+
function applyMaterializedCrdtOperation(tx, state, basePath, path, op) {
|
|
1517
|
+
if (op.kind === 'toggle' && op.repeat % 2 === 0)
|
|
1518
|
+
return;
|
|
1519
|
+
if (op.kind === 'test') {
|
|
1520
|
+
assertMutationExpected(readPath(state, path), op.expected, path);
|
|
1521
|
+
return;
|
|
1522
|
+
}
|
|
1523
|
+
if (op.kind === 'copy') {
|
|
1524
|
+
tx.set(resolveOperationToPath(basePath, op), cloneJson(readRequiredPath(state, path)));
|
|
1525
|
+
return;
|
|
1526
|
+
}
|
|
1527
|
+
if (op.kind === 'move') {
|
|
1528
|
+
const to = resolveOperationToPath(basePath, op);
|
|
1529
|
+
tx.set(to, cloneJson(readRequiredPath(state, path)));
|
|
1530
|
+
tx.delete(path);
|
|
1531
|
+
return;
|
|
1532
|
+
}
|
|
1533
|
+
if (op.kind === 'rename') {
|
|
1534
|
+
const to = renameTargetPath(path, op);
|
|
1535
|
+
tx.set(to, cloneJson(readRequiredPath(state, path)));
|
|
1536
|
+
tx.delete(path);
|
|
1537
|
+
return;
|
|
1538
|
+
}
|
|
1539
|
+
if (isRemoveOperation(op)) {
|
|
1540
|
+
tx.delete(path);
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
1543
|
+
const current = readPath(state, path);
|
|
1544
|
+
if (op.kind === 'ensure' && current !== undefined)
|
|
1545
|
+
return;
|
|
1546
|
+
tx.set(path, cloneJson(applyFieldMutation(current, op, path)));
|
|
1547
|
+
}
|
|
1548
|
+
function applyNativeTextCrdtOperation(tx, state, path, op) {
|
|
1549
|
+
const current = readPath(state, path);
|
|
1550
|
+
if (typeof current !== 'string') {
|
|
1551
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1552
|
+
return;
|
|
463
1553
|
}
|
|
464
|
-
|
|
1554
|
+
const handle = tx.text(path);
|
|
1555
|
+
if (isTextAppendOperation(op)) {
|
|
1556
|
+
const text = (op.text || '').repeat(op.repeat);
|
|
1557
|
+
if (text.length !== 0)
|
|
1558
|
+
handle.insert(Array.from(current).length, text);
|
|
1559
|
+
return;
|
|
1560
|
+
}
|
|
1561
|
+
if (!isTextSpliceOperation(op)) {
|
|
1562
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1563
|
+
return;
|
|
1564
|
+
}
|
|
1565
|
+
let text = current;
|
|
1566
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1567
|
+
const start = boundedIndex(op.start || 0, text.length);
|
|
1568
|
+
const deleteCount = Math.min(op.deleteCount || 0, text.length - start);
|
|
1569
|
+
const insert = textInsertForOperation(op);
|
|
1570
|
+
if (deleteCount !== 0 || insert.length !== 0)
|
|
1571
|
+
handle.splice(start, deleteCount, insert);
|
|
1572
|
+
text = text.slice(0, start) + insert + text.slice(start + deleteCount);
|
|
1573
|
+
}
|
|
1574
|
+
}
|
|
1575
|
+
function applyNativeListCrdtOperation(tx, state, path, op) {
|
|
1576
|
+
const current = readPath(state, path);
|
|
1577
|
+
if (!Array.isArray(current)) {
|
|
1578
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1579
|
+
return;
|
|
1580
|
+
}
|
|
1581
|
+
const handle = tx.list(path);
|
|
1582
|
+
if (op.kind === 'append') {
|
|
1583
|
+
const values = repeatValues(op.values || [], op.repeat);
|
|
1584
|
+
if (values.length !== 0)
|
|
1585
|
+
handle.insert(current.length, values);
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (op.kind === 'prepend') {
|
|
1589
|
+
const values = repeatValues(op.values || [], op.repeat);
|
|
1590
|
+
if (values.length !== 0)
|
|
1591
|
+
handle.insert(0, values);
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
if (op.kind === 'insert') {
|
|
1595
|
+
const values = repeatValues(op.values || [], op.repeat);
|
|
1596
|
+
if (values.length !== 0)
|
|
1597
|
+
handle.insert(boundedIndex(op.start || 0, current.length), values);
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
if (op.kind === 'removeAt') {
|
|
1601
|
+
const start = boundedIndex(op.start || 0, current.length);
|
|
1602
|
+
const deleteCount = Math.min((op.deleteCount || 0) * op.repeat, current.length - start);
|
|
1603
|
+
if (deleteCount !== 0)
|
|
1604
|
+
handle.delete(start, deleteCount);
|
|
1605
|
+
return;
|
|
1606
|
+
}
|
|
1607
|
+
if (op.kind === 'moveItem') {
|
|
1608
|
+
const count = op.deleteCount || 1;
|
|
1609
|
+
if (count !== 0)
|
|
1610
|
+
handle.move(boundedIndex(op.start || 0, current.length), boundedIndex(op.toIndex || 0, current.length), count);
|
|
1611
|
+
return;
|
|
1612
|
+
}
|
|
1613
|
+
if (op.kind !== 'splice') {
|
|
1614
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1615
|
+
return;
|
|
1616
|
+
}
|
|
1617
|
+
const array = cloneJson(current);
|
|
1618
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1619
|
+
const start = boundedIndex(op.start || 0, array.length);
|
|
1620
|
+
const deleteCount = Math.min(op.deleteCount || 0, array.length - start);
|
|
1621
|
+
const values = cloneJson(op.values || []);
|
|
1622
|
+
if (deleteCount !== 0)
|
|
1623
|
+
handle.delete(start, deleteCount);
|
|
1624
|
+
if (values.length !== 0)
|
|
1625
|
+
handle.insert(start, values);
|
|
1626
|
+
array.splice(start, deleteCount, ...values);
|
|
1627
|
+
}
|
|
1628
|
+
}
|
|
1629
|
+
function applyNativeMapFieldCrdtOperation(tx, state, path, op) {
|
|
1630
|
+
if (op.kind === 'assign') {
|
|
465
1631
|
const current = readPath(state, path);
|
|
466
|
-
|
|
467
|
-
|
|
1632
|
+
if (!isObjectRecord(current) || !isObjectRecord(op.value)) {
|
|
1633
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const map = tx.map(path);
|
|
1637
|
+
const value = cloneJson(op.value);
|
|
1638
|
+
for (const key of Object.keys(value))
|
|
1639
|
+
map.set(key, value[key]);
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (path.length === 0 || !isMapFieldMutationOperation(op)) {
|
|
1643
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1644
|
+
return;
|
|
1645
|
+
}
|
|
1646
|
+
const key = path[path.length - 1];
|
|
1647
|
+
if (typeof key !== 'string' && typeof key !== 'number') {
|
|
1648
|
+
applyMaterializedCrdtOperation(tx, state, [], path, op);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
tx.map(path.slice(0, -1)).set(key, cloneJson(applyFieldMutation(readPath(state, path), op, path)));
|
|
1652
|
+
}
|
|
1653
|
+
function canUseNativeCrdtCounter(path, operationCount, materializedWrites) {
|
|
1654
|
+
return (path.length !== 1 || operationCount === 1) && !hasOverlappingPath(materializedWrites, path);
|
|
1655
|
+
}
|
|
1656
|
+
function canUseNativeCrdtText(state, path, op, materializedWrites, nativeSequenceWrites, nativeSequenceBackings) {
|
|
1657
|
+
const current = readPath(state, path);
|
|
1658
|
+
if (typeof current !== 'string' || hasOverlappingPath(materializedWrites, path))
|
|
1659
|
+
return false;
|
|
1660
|
+
if (hasNonExactOverlappingPath(nativeSequenceWrites, path))
|
|
1661
|
+
return false;
|
|
1662
|
+
if (!hasUsableNativeCrdtSequenceBacking(nativeSequenceBackings.text, path, current.length))
|
|
1663
|
+
return false;
|
|
1664
|
+
return !isTextSpliceOperation(op) || (isCodeUnitStableText(current) && isCodeUnitStableText(op.text || ''));
|
|
1665
|
+
}
|
|
1666
|
+
function canUseNativeCrdtList(state, path, materializedWrites, nativeSequenceWrites, nativeSequenceBackings) {
|
|
1667
|
+
const current = readPath(state, path);
|
|
1668
|
+
return Array.isArray(current) &&
|
|
1669
|
+
!hasOverlappingPath(materializedWrites, path) &&
|
|
1670
|
+
!hasNonExactOverlappingPath(nativeSequenceWrites, path) &&
|
|
1671
|
+
hasUsableNativeCrdtSequenceBacking(nativeSequenceBackings.list, path, current.length);
|
|
1672
|
+
}
|
|
1673
|
+
function canUseNativeCrdtMapField(state, path, op, assignmentPolicy, materializedWrites, nativeSequenceWrites) {
|
|
1674
|
+
if (assignmentPolicy !== 'preserve-conflicts')
|
|
1675
|
+
return false;
|
|
1676
|
+
if (!isMapFieldMutationOperation(op))
|
|
1677
|
+
return false;
|
|
1678
|
+
if (hasOverlappingPath(materializedWrites, path) || hasOverlappingPath(nativeSequenceWrites, path))
|
|
1679
|
+
return false;
|
|
1680
|
+
if (op.kind === 'assign')
|
|
1681
|
+
return isObjectRecord(readPath(state, path)) && isObjectRecord(op.value);
|
|
1682
|
+
if (path.length === 0)
|
|
1683
|
+
return false;
|
|
1684
|
+
return isObjectRecord(readPath(state, path.slice(0, -1)));
|
|
1685
|
+
}
|
|
1686
|
+
function trackCrdtWrite(materializedWrites, nativeSequenceWrites, path, op, decision) {
|
|
1687
|
+
if (decision.strategy === 'noop')
|
|
1688
|
+
return;
|
|
1689
|
+
if (isNativeSequenceCrdtDecisionStrategy(decision.strategy))
|
|
1690
|
+
nativeSequenceWrites.push(path.slice());
|
|
1691
|
+
else if (decision.strategy === 'native-delete')
|
|
1692
|
+
materializedWrites.push(path.slice());
|
|
1693
|
+
else if (decision.strategy === 'native-map-field' && op.kind === 'assign' && isObjectRecord(op.value)) {
|
|
1694
|
+
for (const key of Object.keys(op.value))
|
|
1695
|
+
materializedWrites.push(path.concat(key));
|
|
468
1696
|
}
|
|
469
1697
|
else {
|
|
470
|
-
|
|
1698
|
+
materializedWrites.push(path.slice());
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
function isNativeSequenceCrdtDecisionStrategy(strategy) {
|
|
1702
|
+
return strategy === 'native-counter' ||
|
|
1703
|
+
strategy === 'native-text' ||
|
|
1704
|
+
strategy === 'native-text-splice' ||
|
|
1705
|
+
strategy === 'native-list';
|
|
1706
|
+
}
|
|
1707
|
+
function collectNativeCrdtSequenceBackings(doc) {
|
|
1708
|
+
const backings = { list: [], text: [] };
|
|
1709
|
+
const operations = doc.changesSince(null);
|
|
1710
|
+
for (const op of operations) {
|
|
1711
|
+
if (isListCrdtOperation(op)) {
|
|
1712
|
+
addExactPath(backings.list, op.path);
|
|
1713
|
+
}
|
|
1714
|
+
else if (isTextCrdtOperation(op)) {
|
|
1715
|
+
addExactPath(backings.text, op.path);
|
|
1716
|
+
}
|
|
1717
|
+
else if (op.type === 'set' || op.type === 'del') {
|
|
1718
|
+
removeOverlappingPaths(backings.list, op.path);
|
|
1719
|
+
removeOverlappingPaths(backings.text, op.path);
|
|
1720
|
+
}
|
|
1721
|
+
else if (op.type === 'mapSetRun' && op.keys !== undefined) {
|
|
1722
|
+
for (const key of op.keys) {
|
|
1723
|
+
const fieldPath = op.path.concat(key);
|
|
1724
|
+
removeOverlappingPaths(backings.list, fieldPath);
|
|
1725
|
+
removeOverlappingPaths(backings.text, fieldPath);
|
|
1726
|
+
}
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
return backings;
|
|
1730
|
+
}
|
|
1731
|
+
function isListCrdtOperation(op) {
|
|
1732
|
+
return op.type === 'listInsert' || op.type === 'listRun' || op.type === 'listDel';
|
|
1733
|
+
}
|
|
1734
|
+
function isTextCrdtOperation(op) {
|
|
1735
|
+
return op.type === 'textInsert' || op.type === 'textRun' || op.type === 'textDel' || op.type === 'textDelRange';
|
|
1736
|
+
}
|
|
1737
|
+
function hasUsableNativeCrdtSequenceBacking(backedPaths, path, currentLength) {
|
|
1738
|
+
return currentLength === 0 || hasExactPath(backedPaths, path);
|
|
1739
|
+
}
|
|
1740
|
+
function addExactPath(paths, path) {
|
|
1741
|
+
if (!hasExactPath(paths, path))
|
|
1742
|
+
paths.push(path.slice());
|
|
1743
|
+
}
|
|
1744
|
+
function hasExactPath(paths, path) {
|
|
1745
|
+
for (const existing of paths) {
|
|
1746
|
+
if (samePath(existing, path))
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
return false;
|
|
1750
|
+
}
|
|
1751
|
+
function removeOverlappingPaths(paths, path) {
|
|
1752
|
+
for (let i = paths.length - 1; i >= 0; i--) {
|
|
1753
|
+
const existing = paths[i];
|
|
1754
|
+
if (samePath(existing, path) || isPathPrefix(existing, path) || isPathPrefix(path, existing))
|
|
1755
|
+
paths.splice(i, 1);
|
|
471
1756
|
}
|
|
472
1757
|
}
|
|
1758
|
+
function hasOverlappingPath(paths, path) {
|
|
1759
|
+
for (const existing of paths) {
|
|
1760
|
+
if (samePath(existing, path) || isPathPrefix(existing, path) || isPathPrefix(path, existing))
|
|
1761
|
+
return true;
|
|
1762
|
+
}
|
|
1763
|
+
return false;
|
|
1764
|
+
}
|
|
1765
|
+
function hasNonExactOverlappingPath(paths, path) {
|
|
1766
|
+
for (const existing of paths) {
|
|
1767
|
+
if (!samePath(existing, path) && (isPathPrefix(existing, path) || isPathPrefix(path, existing)))
|
|
1768
|
+
return true;
|
|
1769
|
+
}
|
|
1770
|
+
return false;
|
|
1771
|
+
}
|
|
473
1772
|
function applyNumericMutation(current, op) {
|
|
474
1773
|
const base = typeof current === 'number' && Number.isFinite(current) ? current : 0;
|
|
475
1774
|
const sign = op.kind === 'decrement' ? -1 : 1;
|
|
@@ -493,58 +1792,649 @@ function applyMutationToValue(current, op, warnings) {
|
|
|
493
1792
|
warnings.push('unsupported materialized mutation kind: ' + op.kind);
|
|
494
1793
|
return current === undefined ? null : cloneJson(current);
|
|
495
1794
|
}
|
|
496
|
-
function
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
1795
|
+
function makeValueOperation(kind, path, value) {
|
|
1796
|
+
if (typeof value === 'function')
|
|
1797
|
+
return { kind, path, factory: value, repeat: 1 };
|
|
1798
|
+
return { kind, path, value, repeat: 1 };
|
|
1799
|
+
}
|
|
1800
|
+
function readOperationValue(op, current, path) {
|
|
1801
|
+
return op.factory === undefined
|
|
1802
|
+
? cloneJson(op.value)
|
|
1803
|
+
: cloneJson(op.factory(current, path.slice()));
|
|
1804
|
+
}
|
|
1805
|
+
function applyUpsertMutation(current, op, path) {
|
|
1806
|
+
const value = readOperationValue(op, current, path);
|
|
1807
|
+
return isObjectRecord(current) && isObjectRecord(value)
|
|
1808
|
+
? { ...current, ...value }
|
|
1809
|
+
: value;
|
|
1810
|
+
}
|
|
1811
|
+
function applyAdvancedNumericMutation(current, op) {
|
|
1812
|
+
let value = typeof current === 'number' && Number.isFinite(current) ? current : 0;
|
|
1813
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1814
|
+
if (op.kind === 'multiply')
|
|
1815
|
+
value *= op.delta;
|
|
1816
|
+
else if (op.kind === 'min')
|
|
1817
|
+
value = Math.min(value, op.delta);
|
|
1818
|
+
else if (op.kind === 'max')
|
|
1819
|
+
value = Math.max(value, op.delta);
|
|
1820
|
+
else if (op.kind === 'clamp')
|
|
1821
|
+
value = Math.min(Math.max(value, op.min), op.max);
|
|
1822
|
+
}
|
|
1823
|
+
return value;
|
|
1824
|
+
}
|
|
1825
|
+
function applyListMutationToValue(current, op) {
|
|
1826
|
+
const array = Array.isArray(current) ? cloneJson(current) : [];
|
|
1827
|
+
if (op.kind === 'addToSet')
|
|
1828
|
+
return array.concat(addToSetValues(array, op));
|
|
1829
|
+
if (op.kind === 'pull')
|
|
1830
|
+
return array.filter((value) => !(op.values || []).some((candidate) => jsonEqual(value, candidate)));
|
|
1831
|
+
if (op.kind === 'removeWhere')
|
|
1832
|
+
return array.filter((value, index) => !op.predicate(value, index, array));
|
|
1833
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1834
|
+
if (op.kind === 'prepend') {
|
|
1835
|
+
array.splice(0, 0, ...cloneJson(op.values || []));
|
|
1836
|
+
}
|
|
1837
|
+
else if (op.kind === 'insert') {
|
|
1838
|
+
array.splice(boundedIndex(op.start || 0, array.length), 0, ...cloneJson(op.values || []));
|
|
1839
|
+
}
|
|
1840
|
+
else if (op.kind === 'removeAt') {
|
|
1841
|
+
const start = boundedIndex(op.start || 0, array.length);
|
|
1842
|
+
array.splice(start, Math.min(op.deleteCount || 0, array.length - start));
|
|
1843
|
+
}
|
|
1844
|
+
else if (op.kind === 'moveItem') {
|
|
1845
|
+
moveArrayItems(array, boundedIndex(op.start || 0, array.length), boundedIndex(op.toIndex || 0, array.length), op.deleteCount || 1);
|
|
1846
|
+
}
|
|
1847
|
+
else if (op.kind === 'splice') {
|
|
1848
|
+
array.splice(boundedIndex(op.start || 0, array.length), op.deleteCount || 0, ...cloneJson(op.values || []));
|
|
1849
|
+
}
|
|
1850
|
+
}
|
|
1851
|
+
return array;
|
|
1852
|
+
}
|
|
1853
|
+
function applyTextMutationToValue(current, op) {
|
|
1854
|
+
let text = typeof current === 'string' ? current : '';
|
|
1855
|
+
if (isTextAppendOperation(op))
|
|
1856
|
+
return text + (op.text || '').repeat(op.repeat);
|
|
1857
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1858
|
+
const start = boundedIndex(op.start || 0, text.length);
|
|
1859
|
+
const deleteCount = Math.min(op.deleteCount || 0, text.length - start);
|
|
1860
|
+
text = text.slice(0, start) + textInsertForOperation(op) + text.slice(start + deleteCount);
|
|
1861
|
+
}
|
|
1862
|
+
return text;
|
|
1863
|
+
}
|
|
1864
|
+
function applyRichTextFormatMutation(current, op) {
|
|
1865
|
+
const attributes = cloneJson(op.attributes || {});
|
|
1866
|
+
const value = isObjectRecord(current) && typeof current.text === 'string'
|
|
1867
|
+
? cloneJson(current)
|
|
1868
|
+
: { text: typeof current === 'string' ? current : '' };
|
|
1869
|
+
const length = op.length || 0;
|
|
1870
|
+
if (length === 0 || Object.keys(attributes).length === 0)
|
|
1871
|
+
return value;
|
|
1872
|
+
const spans = Array.isArray(value.spans) ? cloneJson(value.spans) : [];
|
|
1873
|
+
spans.push({
|
|
1874
|
+
start: op.start || 0,
|
|
1875
|
+
end: (op.start || 0) + length,
|
|
1876
|
+
attributes
|
|
1877
|
+
});
|
|
1878
|
+
value.spans = spans;
|
|
1879
|
+
return value;
|
|
1880
|
+
}
|
|
1881
|
+
function listSpliceArgs(current, op) {
|
|
1882
|
+
const length = Array.isArray(current) ? current.length : 0;
|
|
1883
|
+
if (op.kind === 'prepend')
|
|
1884
|
+
return { start: 0, deleteCount: 0, values: repeatValues(op.values || [], op.repeat) };
|
|
1885
|
+
if (op.kind === 'insert')
|
|
1886
|
+
return { start: boundedIndex(op.start || 0, length), deleteCount: 0, values: repeatValues(op.values || [], op.repeat) };
|
|
1887
|
+
if (op.kind === 'removeAt') {
|
|
1888
|
+
const start = boundedIndex(op.start || 0, length);
|
|
1889
|
+
return { start, deleteCount: Math.min((op.deleteCount || 0) * op.repeat, length - start), values: [] };
|
|
1890
|
+
}
|
|
1891
|
+
const start = boundedIndex(op.start || 0, length);
|
|
1892
|
+
return { start, deleteCount: Math.min(op.deleteCount || 0, length - start), values: repeatValues(op.values || [], op.repeat) };
|
|
1893
|
+
}
|
|
1894
|
+
function addToSetValues(current, op) {
|
|
1895
|
+
const existing = Array.isArray(current) ? current : [];
|
|
1896
|
+
const out = [];
|
|
1897
|
+
for (let i = 0; i < op.repeat; i++) {
|
|
1898
|
+
for (const value of op.values || []) {
|
|
1899
|
+
if (existing.some((item) => jsonEqual(item, value)) || out.some((item) => jsonEqual(item, value)))
|
|
1900
|
+
continue;
|
|
1901
|
+
out.push(cloneJson(value));
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return out;
|
|
1905
|
+
}
|
|
1906
|
+
function moveArrayItems(array, from, to, count) {
|
|
1907
|
+
if (count === 0 || from === to || from >= array.length)
|
|
1908
|
+
return;
|
|
1909
|
+
const deleteCount = Math.min(count, array.length - from);
|
|
1910
|
+
const values = array.splice(from, deleteCount);
|
|
1911
|
+
const target = from < to ? Math.max(0, to - deleteCount) : to;
|
|
1912
|
+
array.splice(boundedIndex(target, array.length), 0, ...values);
|
|
1913
|
+
}
|
|
1914
|
+
function textStartForOperation(current, op) {
|
|
1915
|
+
const length = typeof current === 'string' ? current.length : 0;
|
|
1916
|
+
if (isTextAppendOperation(op))
|
|
1917
|
+
return length;
|
|
1918
|
+
return boundedIndex(op.start || 0, length);
|
|
1919
|
+
}
|
|
1920
|
+
function textDeleteCountForOperation(current, op, start) {
|
|
1921
|
+
const length = typeof current === 'string' ? current.length : 0;
|
|
1922
|
+
return Math.min(op.deleteCount || 0, Math.max(0, length - start));
|
|
1923
|
+
}
|
|
1924
|
+
function textInsertForOperation(op) {
|
|
1925
|
+
if (op.kind === 'appendText')
|
|
1926
|
+
return (op.text || '').repeat(op.repeat);
|
|
1927
|
+
return op.kind === 'deleteText' ? '' : op.text || '';
|
|
1928
|
+
}
|
|
1929
|
+
function isTextAppendOperation(op) {
|
|
1930
|
+
return op.kind === 'appendText';
|
|
1931
|
+
}
|
|
1932
|
+
function isTextSpliceOperation(op) {
|
|
1933
|
+
return op.kind === 'spliceText' || op.kind === 'insertText' || op.kind === 'deleteText' || op.kind === 'replaceText';
|
|
1934
|
+
}
|
|
1935
|
+
function isListInsertOperation(op) {
|
|
1936
|
+
return op.kind === 'prepend' || op.kind === 'insert';
|
|
1937
|
+
}
|
|
1938
|
+
function isListSpliceOperation(op) {
|
|
1939
|
+
return op.kind === 'splice' || op.kind === 'removeAt';
|
|
1940
|
+
}
|
|
1941
|
+
function isNativeListCandidateOperation(op) {
|
|
1942
|
+
return op.kind === 'append' ||
|
|
1943
|
+
op.kind === 'prepend' ||
|
|
1944
|
+
op.kind === 'splice' ||
|
|
1945
|
+
op.kind === 'insert' ||
|
|
1946
|
+
op.kind === 'removeAt' ||
|
|
1947
|
+
op.kind === 'moveItem';
|
|
1948
|
+
}
|
|
1949
|
+
function isRemoveOperation(op) {
|
|
1950
|
+
return op.kind === 'remove' || op.kind === 'unset';
|
|
1951
|
+
}
|
|
1952
|
+
function resolveOperationToPath(basePath, op) {
|
|
1953
|
+
if (op.to === undefined)
|
|
1954
|
+
throw new TypeError(op.kind + ' requires a target path');
|
|
1955
|
+
return basePath.concat(op.to);
|
|
1956
|
+
}
|
|
1957
|
+
function renameTargetPath(path, op) {
|
|
1958
|
+
if (path.length === 0)
|
|
1959
|
+
throw new TypeError('cannot rename the root value');
|
|
1960
|
+
return path.slice(0, -1).concat(op.key);
|
|
1961
|
+
}
|
|
1962
|
+
function mutationDirtyPaths(basePath, path, op) {
|
|
1963
|
+
if (op.kind === 'test')
|
|
1964
|
+
return [];
|
|
1965
|
+
if (op.kind === 'copy')
|
|
1966
|
+
return [resolveOperationToPath(basePath, op)];
|
|
1967
|
+
if (op.kind === 'move')
|
|
1968
|
+
return pathsEqualOrPair(path, resolveOperationToPath(basePath, op));
|
|
1969
|
+
if (op.kind === 'rename')
|
|
1970
|
+
return pathsEqualOrPair(path, renameTargetPath(path, op));
|
|
1971
|
+
if (op.kind === 'ensure')
|
|
1972
|
+
return [path.slice()];
|
|
1973
|
+
return [path.slice()];
|
|
1974
|
+
}
|
|
1975
|
+
function pathsEqualOrPair(left, right) {
|
|
1976
|
+
return samePath(left, right) ? [left.slice()] : [left.slice(), right.slice()];
|
|
1977
|
+
}
|
|
1978
|
+
function emitRemovePatch(source, path, patch) {
|
|
1979
|
+
if (path.length === 0)
|
|
1980
|
+
throw new TypeError('cannot remove the root value');
|
|
1981
|
+
const parentPath = path.slice(0, -1);
|
|
1982
|
+
const key = path[path.length - 1];
|
|
1983
|
+
const parent = readPath(source, parentPath);
|
|
1984
|
+
if (Array.isArray(parent) && typeof key === 'number') {
|
|
1985
|
+
patch.push([OP_ARRAY_SPLICE, parentPath, boundedIndex(key, parent.length), key < parent.length ? 1 : 0, []]);
|
|
1986
|
+
}
|
|
1987
|
+
else {
|
|
1988
|
+
patch.push([OP_REMOVE, path.slice()]);
|
|
1989
|
+
}
|
|
1990
|
+
}
|
|
1991
|
+
function emitMovePatch(source, from, to, patch) {
|
|
1992
|
+
if (samePath(from, to))
|
|
1993
|
+
return;
|
|
1994
|
+
if (from.length === 0)
|
|
1995
|
+
throw new TypeError('cannot move the root value');
|
|
1996
|
+
if (isPathPrefix(from, to))
|
|
1997
|
+
throw new TypeError('cannot move a value into its own descendant');
|
|
1998
|
+
const fromParent = from.slice(0, -1);
|
|
1999
|
+
const toParent = to.slice(0, -1);
|
|
2000
|
+
const fromKey = from[from.length - 1];
|
|
2001
|
+
const toKey = to[to.length - 1];
|
|
2002
|
+
const sourceParent = readPath(source, fromParent);
|
|
2003
|
+
if (Array.isArray(sourceParent) && samePath(fromParent, toParent) && typeof fromKey === 'number' && typeof toKey === 'number') {
|
|
2004
|
+
patch.push([OP_ARRAY_MOVE, fromParent, boundedIndex(fromKey, sourceParent.length), boundedIndex(toKey, sourceParent.length)]);
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
patch.push([OP_SET, to.slice(), cloneJson(readRequiredPath(source, from))]);
|
|
2008
|
+
emitRemovePatch(source, from, patch);
|
|
2009
|
+
}
|
|
2010
|
+
function setPathInWorking(working, path, value) {
|
|
2011
|
+
return applyPatch(working, [[OP_SET, path.slice(), cloneJson(value)]]);
|
|
2012
|
+
}
|
|
2013
|
+
function removePathFromWorking(working, path) {
|
|
2014
|
+
const patch = [];
|
|
2015
|
+
emitRemovePatch(working, path, patch);
|
|
2016
|
+
return patch.length === 0 ? working : applyPatch(working, patch);
|
|
2017
|
+
}
|
|
2018
|
+
function movePathInWorking(working, from, to) {
|
|
2019
|
+
const patch = [];
|
|
2020
|
+
emitMovePatch(working, from, to, patch);
|
|
2021
|
+
return patch.length === 0 ? working : applyPatch(working, patch, { cloneValues: true });
|
|
2022
|
+
}
|
|
2023
|
+
function assertMutationExpected(actual, expected, path) {
|
|
2024
|
+
if (!jsonEqual(actual, expected)) {
|
|
2025
|
+
throw new TypeError('mutation precondition failed at ' + JSON.stringify(path));
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
function jsonEqual(left, right) {
|
|
2029
|
+
if (Object.is(left, right))
|
|
2030
|
+
return true;
|
|
2031
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
2032
|
+
if (!Array.isArray(left) || !Array.isArray(right) || left.length !== right.length)
|
|
2033
|
+
return false;
|
|
2034
|
+
for (let i = 0; i < left.length; i++) {
|
|
2035
|
+
if (!jsonEqual(left[i], right[i]))
|
|
2036
|
+
return false;
|
|
2037
|
+
}
|
|
2038
|
+
return true;
|
|
2039
|
+
}
|
|
2040
|
+
if (isObjectRecord(left) || isObjectRecord(right)) {
|
|
2041
|
+
if (!isObjectRecord(left) || !isObjectRecord(right))
|
|
2042
|
+
return false;
|
|
2043
|
+
const leftKeys = Object.keys(left);
|
|
2044
|
+
const rightKeys = Object.keys(right);
|
|
2045
|
+
if (leftKeys.length !== rightKeys.length)
|
|
2046
|
+
return false;
|
|
2047
|
+
for (const key of leftKeys) {
|
|
2048
|
+
if (!Object.prototype.hasOwnProperty.call(right, key) || !jsonEqual(left[key], right[key]))
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
return true;
|
|
2052
|
+
}
|
|
2053
|
+
return false;
|
|
2054
|
+
}
|
|
2055
|
+
function createMutationRuntime(schema) {
|
|
2056
|
+
const tables = new Map();
|
|
2057
|
+
for (const table of schema.tables) {
|
|
2058
|
+
tables.set(pathCacheKey(table.path), {
|
|
2059
|
+
schema: table,
|
|
2060
|
+
version: 0,
|
|
2061
|
+
keyIndexVersion: -1
|
|
2062
|
+
});
|
|
502
2063
|
}
|
|
503
|
-
|
|
504
|
-
|
|
2064
|
+
return { schema, tables, selectorCache: [] };
|
|
2065
|
+
}
|
|
2066
|
+
function resolveSelector(source, selector, runtime) {
|
|
2067
|
+
const wildcardIndexes = readSelectorWildcardIndexes(selector.path);
|
|
2068
|
+
const firstWildcard = wildcardIndexes[0];
|
|
2069
|
+
const basePath = selector.path.slice(0, firstWildcard);
|
|
2070
|
+
const tailPath = wildcardIndexes.length === 1 ? selector.path.slice(firstWildcard + 1) : [];
|
|
505
2071
|
const collection = readPath(source, basePath);
|
|
2072
|
+
const tableState = runtime === undefined ? undefined : findSelectorTableState(runtime, selector, wildcardIndexes);
|
|
2073
|
+
const cached = runtime === undefined || tableState === undefined
|
|
2074
|
+
? undefined
|
|
2075
|
+
: readCachedSelectorContext(runtime, tableState, selector);
|
|
2076
|
+
if (cached !== undefined)
|
|
2077
|
+
return cached;
|
|
506
2078
|
const matches = [];
|
|
2079
|
+
const state = { collectionSize: 0, usedIndex: false };
|
|
2080
|
+
walkSelectorPath(source, selector, source, [], 0, undefined, matches, state, tableState);
|
|
2081
|
+
applySelectorOrderingAndLimit(matches, selector);
|
|
2082
|
+
const context = {
|
|
2083
|
+
selector,
|
|
2084
|
+
basePath,
|
|
2085
|
+
tailPath,
|
|
2086
|
+
collection: collection,
|
|
2087
|
+
collectionSize: state.collectionSize,
|
|
2088
|
+
wildcardCount: wildcardIndexes.length,
|
|
2089
|
+
matches,
|
|
2090
|
+
schema: tableState?.schema,
|
|
2091
|
+
usedIndex: state.usedIndex
|
|
2092
|
+
};
|
|
2093
|
+
if (runtime !== undefined && tableState !== undefined && canCacheSelectorContext(tableState.schema, selector)) {
|
|
2094
|
+
runtime.selectorCache.push({
|
|
2095
|
+
selector: cloneSelectorPlan(selector),
|
|
2096
|
+
tablePathKey: pathCacheKey(tableState.schema.path),
|
|
2097
|
+
version: tableState.version,
|
|
2098
|
+
context
|
|
2099
|
+
});
|
|
2100
|
+
}
|
|
2101
|
+
return context;
|
|
2102
|
+
}
|
|
2103
|
+
function findSelectorTableState(runtime, selector, wildcardIndexes) {
|
|
2104
|
+
if (wildcardIndexes.length !== 1)
|
|
2105
|
+
return undefined;
|
|
2106
|
+
const tablePath = selector.path.slice(0, wildcardIndexes[0]);
|
|
2107
|
+
return runtime.tables.get(pathCacheKey(tablePath));
|
|
2108
|
+
}
|
|
2109
|
+
function readCachedSelectorContext(runtime, tableState, selector) {
|
|
2110
|
+
if (!canCacheSelectorContext(tableState.schema, selector))
|
|
2111
|
+
return undefined;
|
|
2112
|
+
const tablePathKey = pathCacheKey(tableState.schema.path);
|
|
2113
|
+
for (const entry of runtime.selectorCache) {
|
|
2114
|
+
if (entry.version === tableState.version &&
|
|
2115
|
+
entry.tablePathKey === tablePathKey &&
|
|
2116
|
+
sameSelectorPlan(entry.selector, selector)) {
|
|
2117
|
+
return { ...entry.context, usedCache: true, usedIndex: entry.context.usedIndex };
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
return undefined;
|
|
2121
|
+
}
|
|
2122
|
+
function canCacheSelectorContext(schema, selector) {
|
|
2123
|
+
if (!schema.stableRowShape || schema.selectorFields.length === 0)
|
|
2124
|
+
return false;
|
|
2125
|
+
const fields = [];
|
|
2126
|
+
collectSelectorConditionFields(selector.conditions, fields);
|
|
2127
|
+
for (const field of fields) {
|
|
2128
|
+
if (!schema.selectorFields.some((schemaField) => samePath(schemaField, field)))
|
|
2129
|
+
return false;
|
|
2130
|
+
}
|
|
2131
|
+
return true;
|
|
2132
|
+
}
|
|
2133
|
+
function noteSelectorMutation(runtime, context, op) {
|
|
2134
|
+
for (const match of context.matches)
|
|
2135
|
+
noteAbsoluteMutation(runtime, match.path.concat(op.path));
|
|
2136
|
+
}
|
|
2137
|
+
function noteAbsoluteMutation(runtime, path) {
|
|
2138
|
+
for (const tableState of runtime.tables.values()) {
|
|
2139
|
+
const rowField = readTableRowFieldPath(tableState.schema.path, path);
|
|
2140
|
+
if (rowField === undefined || tableMutationPreservesIndexesAndSelectors(tableState.schema, rowField))
|
|
2141
|
+
continue;
|
|
2142
|
+
tableState.version++;
|
|
2143
|
+
tableState.keyIndex = undefined;
|
|
2144
|
+
tableState.keyIndexVersion = -1;
|
|
2145
|
+
}
|
|
2146
|
+
}
|
|
2147
|
+
function readTableRowFieldPath(tablePath, path) {
|
|
2148
|
+
if (samePath(path, tablePath) || isPathPrefix(path, tablePath))
|
|
2149
|
+
return [];
|
|
2150
|
+
if (!isPathPrefix(tablePath, path))
|
|
2151
|
+
return undefined;
|
|
2152
|
+
const rest = path.slice(tablePath.length);
|
|
2153
|
+
if (rest.length < 2)
|
|
2154
|
+
return [];
|
|
2155
|
+
return rest.slice(1);
|
|
2156
|
+
}
|
|
2157
|
+
function tableMutationPreservesIndexesAndSelectors(schema, rowField) {
|
|
2158
|
+
if (rowField.length === 0)
|
|
2159
|
+
return false;
|
|
2160
|
+
if (schema.key !== undefined && overlappingMutationPaths(rowField, schema.key))
|
|
2161
|
+
return false;
|
|
2162
|
+
for (const field of schema.selectorFields) {
|
|
2163
|
+
if (overlappingMutationPaths(rowField, field))
|
|
2164
|
+
return false;
|
|
2165
|
+
}
|
|
2166
|
+
return true;
|
|
2167
|
+
}
|
|
2168
|
+
function schemaFieldKind(schema, field) {
|
|
2169
|
+
if (schema.numericFields.some((candidate) => samePath(candidate, field)))
|
|
2170
|
+
return 'numeric';
|
|
2171
|
+
if (schema.textFields.some((candidate) => samePath(candidate, field)))
|
|
2172
|
+
return 'text';
|
|
2173
|
+
if (schema.listFields.some((candidate) => samePath(candidate, field)))
|
|
2174
|
+
return 'list';
|
|
2175
|
+
return undefined;
|
|
2176
|
+
}
|
|
2177
|
+
function readSelectorWildcardIndexes(path) {
|
|
2178
|
+
const indexes = [];
|
|
2179
|
+
for (let i = 0; i < path.length; i++) {
|
|
2180
|
+
if (path[i] === '*')
|
|
2181
|
+
indexes.push(i);
|
|
2182
|
+
}
|
|
2183
|
+
if (indexes.length === 0)
|
|
2184
|
+
throw new TypeError('selector path must contain at least one * segment');
|
|
2185
|
+
return indexes;
|
|
2186
|
+
}
|
|
2187
|
+
function walkSelectorPath(source, selector, value, absolutePath, pathIndex, meta, matches, state, tableState) {
|
|
2188
|
+
if (pathIndex >= selector.path.length) {
|
|
2189
|
+
if (matchesSelector(value, selector.conditions, meta)) {
|
|
2190
|
+
const fallback = meta === undefined ? readFallbackSelectorKey(absolutePath) : meta.key;
|
|
2191
|
+
matches.push({
|
|
2192
|
+
key: readSelectorMatchKey(value, selector, fallback, meta, tableState?.schema),
|
|
2193
|
+
rowIndex: meta?.rowIndex,
|
|
2194
|
+
mapKey: meta?.mapKey,
|
|
2195
|
+
value: value,
|
|
2196
|
+
path: absolutePath.slice()
|
|
2197
|
+
});
|
|
2198
|
+
}
|
|
2199
|
+
return;
|
|
2200
|
+
}
|
|
2201
|
+
const segment = selector.path[pathIndex];
|
|
2202
|
+
if (segment !== '*') {
|
|
2203
|
+
walkSelectorPath(source, selector, readPath(value, [segment]), absolutePath.concat(segment), pathIndex + 1, meta, matches, state, tableState);
|
|
2204
|
+
return;
|
|
2205
|
+
}
|
|
2206
|
+
const isFinalWildcard = selector.path.indexOf('*', pathIndex + 1) === -1;
|
|
2207
|
+
state.collectionSize += isFinalWildcard ? collectionSizeOf(value) : 0;
|
|
2208
|
+
const candidateTableState = isFinalWildcard && tableState !== undefined && samePath(tableState.schema.path, absolutePath)
|
|
2209
|
+
? tableState
|
|
2210
|
+
: undefined;
|
|
2211
|
+
const candidates = enumerateSelectorCandidates(value, selector, isFinalWildcard, candidateTableState, state);
|
|
2212
|
+
for (const candidate of candidates) {
|
|
2213
|
+
const nextMeta = isFinalWildcard
|
|
2214
|
+
? { key: candidate.key, rowIndex: candidate.rowIndex, mapKey: candidate.mapKey }
|
|
2215
|
+
: meta;
|
|
2216
|
+
walkSelectorPath(source, selector, candidate.value, absolutePath.concat(candidate.key), pathIndex + 1, nextMeta, matches, state, tableState);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
function enumerateSelectorCandidates(collection, selector, useHints, tableState, state) {
|
|
507
2220
|
if (Array.isArray(collection)) {
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
2221
|
+
const indexHint = useHints ? readArrayIndexHint(selector.conditions) : undefined;
|
|
2222
|
+
const indexedCandidates = indexHint === undefined && useHints
|
|
2223
|
+
? enumerateArrayIndexedCandidates(collection, selector, tableState, state)
|
|
2224
|
+
: undefined;
|
|
2225
|
+
if (indexedCandidates !== undefined)
|
|
2226
|
+
return indexedCandidates;
|
|
2227
|
+
const indexes = indexHint === undefined ? makeArrayIndexes(collection.length) : indexHint;
|
|
2228
|
+
const out = [];
|
|
2229
|
+
for (const index of indexes) {
|
|
2230
|
+
if (Number.isSafeInteger(index) && index >= 0 && index < collection.length) {
|
|
2231
|
+
out.push({ key: index, rowIndex: index, value: collection[index] });
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
return out;
|
|
2235
|
+
}
|
|
2236
|
+
if (isObjectRecord(collection)) {
|
|
2237
|
+
const keyHint = useHints ? readObjectKeyHint(selector, collection, tableState?.schema) : undefined;
|
|
2238
|
+
if (keyHint !== undefined)
|
|
2239
|
+
state.usedIndex = true;
|
|
2240
|
+
const keys = keyHint === undefined ? Object.keys(collection) : keyHint;
|
|
2241
|
+
const out = [];
|
|
2242
|
+
for (const key of keys) {
|
|
2243
|
+
if (Object.prototype.hasOwnProperty.call(collection, key)) {
|
|
2244
|
+
out.push({ key, mapKey: key, value: collection[key] });
|
|
513
2245
|
}
|
|
514
2246
|
}
|
|
2247
|
+
return out;
|
|
515
2248
|
}
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
2249
|
+
return [];
|
|
2250
|
+
}
|
|
2251
|
+
function makeArrayIndexes(length) {
|
|
2252
|
+
const indexes = new Array(length);
|
|
2253
|
+
for (let i = 0; i < length; i++)
|
|
2254
|
+
indexes[i] = i;
|
|
2255
|
+
return indexes;
|
|
2256
|
+
}
|
|
2257
|
+
function readSelectorIndexPath(selector, schema) {
|
|
2258
|
+
return selector.indexBy || selector.keyBy || schema?.key;
|
|
2259
|
+
}
|
|
2260
|
+
function readTableKeyIndex(tableState, collection) {
|
|
2261
|
+
if (tableState.keyIndex !== undefined && tableState.keyIndexVersion === tableState.version)
|
|
2262
|
+
return tableState.keyIndex;
|
|
2263
|
+
const index = new Map();
|
|
2264
|
+
const keyPath = tableState.schema.key;
|
|
2265
|
+
if (keyPath !== undefined) {
|
|
2266
|
+
for (let rowIndex = 0; rowIndex < collection.length; rowIndex++) {
|
|
2267
|
+
const key = readPath(collection[rowIndex], keyPath);
|
|
2268
|
+
if (typeof key !== 'string' && typeof key !== 'number')
|
|
2269
|
+
continue;
|
|
2270
|
+
const rows = index.get(key);
|
|
2271
|
+
if (rows === undefined)
|
|
2272
|
+
index.set(key, [rowIndex]);
|
|
2273
|
+
else
|
|
2274
|
+
rows.push(rowIndex);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
tableState.keyIndex = index;
|
|
2278
|
+
tableState.keyIndexVersion = tableState.version;
|
|
2279
|
+
return index;
|
|
2280
|
+
}
|
|
2281
|
+
function enumerateArrayIndexedCandidates(collection, selector, tableState, state) {
|
|
2282
|
+
const indexPath = readSelectorIndexPath(selector, tableState?.schema);
|
|
2283
|
+
if (indexPath === undefined || isSpecialSelectorPath(indexPath))
|
|
2284
|
+
return undefined;
|
|
2285
|
+
const values = readSelectorEqualityHint(selector.conditions, indexPath);
|
|
2286
|
+
if (values === undefined)
|
|
2287
|
+
return undefined;
|
|
2288
|
+
if (tableState !== undefined && tableState.schema.key !== undefined && samePath(indexPath, tableState.schema.key)) {
|
|
2289
|
+
const index = readTableKeyIndex(tableState, collection);
|
|
2290
|
+
const out = [];
|
|
2291
|
+
for (const value of values) {
|
|
2292
|
+
const rows = index.get(value);
|
|
2293
|
+
if (rows === undefined)
|
|
2294
|
+
continue;
|
|
2295
|
+
for (const rowIndex of rows) {
|
|
2296
|
+
if (rowIndex >= 0 && rowIndex < collection.length) {
|
|
2297
|
+
out.push({ key: rowIndex, rowIndex, value: collection[rowIndex] });
|
|
2298
|
+
}
|
|
522
2299
|
}
|
|
523
2300
|
}
|
|
2301
|
+
state.usedIndex = true;
|
|
2302
|
+
return out;
|
|
2303
|
+
}
|
|
2304
|
+
const out = [];
|
|
2305
|
+
const valueSet = new Set(values);
|
|
2306
|
+
for (let index = 0; index < collection.length; index++) {
|
|
2307
|
+
const row = collection[index];
|
|
2308
|
+
const key = readPath(row, indexPath);
|
|
2309
|
+
if ((typeof key === 'string' || typeof key === 'number') && valueSet.has(key)) {
|
|
2310
|
+
out.push({ key: index, rowIndex: index, value: row });
|
|
2311
|
+
}
|
|
524
2312
|
}
|
|
525
|
-
|
|
2313
|
+
state.usedIndex = true;
|
|
2314
|
+
return out;
|
|
526
2315
|
}
|
|
527
|
-
function
|
|
528
|
-
|
|
2316
|
+
function readArrayIndexHint(conditions) {
|
|
2317
|
+
const values = readSelectorEqualityHint(conditions, ['$index']);
|
|
2318
|
+
if (values === undefined)
|
|
2319
|
+
return undefined;
|
|
2320
|
+
const indexes = [];
|
|
2321
|
+
for (const value of values) {
|
|
2322
|
+
if (typeof value === 'number' && Number.isSafeInteger(value) && value >= 0)
|
|
2323
|
+
indexes.push(value);
|
|
2324
|
+
}
|
|
2325
|
+
return indexes.length === 0 ? undefined : uniqueNumbers(indexes);
|
|
2326
|
+
}
|
|
2327
|
+
function readObjectKeyHint(selector, collection, schema) {
|
|
2328
|
+
const keyValues = readSelectorEqualityHint(selector.conditions, ['$key']);
|
|
2329
|
+
if (keyValues !== undefined)
|
|
2330
|
+
return uniqueStrings(keyValues.map(String));
|
|
2331
|
+
const indexPath = readSelectorIndexPath(selector, schema);
|
|
2332
|
+
if (indexPath === undefined || isSpecialSelectorPath(indexPath))
|
|
2333
|
+
return undefined;
|
|
2334
|
+
const indexValues = readSelectorEqualityHint(selector.conditions, indexPath);
|
|
2335
|
+
if (indexValues === undefined)
|
|
2336
|
+
return undefined;
|
|
2337
|
+
const keys = uniqueStrings(indexValues.map(String));
|
|
2338
|
+
return keys.some((key) => Object.prototype.hasOwnProperty.call(collection, key)) ? keys : undefined;
|
|
2339
|
+
}
|
|
2340
|
+
function readSelectorEqualityHint(conditions, field) {
|
|
2341
|
+
let values;
|
|
2342
|
+
for (const condition of conditions) {
|
|
2343
|
+
const next = readConditionEqualityHint(condition, field);
|
|
2344
|
+
if (next === undefined)
|
|
2345
|
+
continue;
|
|
2346
|
+
values = values === undefined ? next : intersectObjectKeys(values, next);
|
|
2347
|
+
}
|
|
2348
|
+
return values;
|
|
2349
|
+
}
|
|
2350
|
+
function readConditionEqualityHint(condition, field) {
|
|
2351
|
+
if ('and' in condition)
|
|
2352
|
+
return readSelectorEqualityHint(condition.and, field);
|
|
2353
|
+
if ('or' in condition) {
|
|
2354
|
+
let values = [];
|
|
2355
|
+
for (const item of condition.or) {
|
|
2356
|
+
const next = readConditionEqualityHint(item, field);
|
|
2357
|
+
if (next === undefined)
|
|
2358
|
+
return undefined;
|
|
2359
|
+
values = values.concat(next);
|
|
2360
|
+
}
|
|
2361
|
+
return uniqueObjectKeys(values);
|
|
2362
|
+
}
|
|
2363
|
+
if ('not' in condition)
|
|
2364
|
+
return undefined;
|
|
2365
|
+
if (!samePath(normalizePath(condition.field, 'condition field'), field))
|
|
2366
|
+
return undefined;
|
|
2367
|
+
const op = normalizeOperator(condition);
|
|
2368
|
+
if (op !== 'eq' && op !== 'in')
|
|
2369
|
+
return undefined;
|
|
2370
|
+
const expected = readExpected(condition, op);
|
|
2371
|
+
const values = op === 'in' && Array.isArray(expected) ? expected : [expected];
|
|
2372
|
+
const keys = [];
|
|
2373
|
+
for (const value of values) {
|
|
2374
|
+
if (typeof value === 'string' || typeof value === 'number')
|
|
2375
|
+
keys.push(value);
|
|
2376
|
+
}
|
|
2377
|
+
return keys.length === 0 ? undefined : uniqueObjectKeys(keys);
|
|
2378
|
+
}
|
|
2379
|
+
function readSelectorMatchKey(row, selector, fallback, meta, schema) {
|
|
2380
|
+
const keyPath = selector.keyBy || schema?.key;
|
|
2381
|
+
if (keyPath === undefined)
|
|
529
2382
|
return fallback;
|
|
530
|
-
const key =
|
|
2383
|
+
const key = readSelectorConditionValue(row, keyPath, meta);
|
|
531
2384
|
return typeof key === 'string' || typeof key === 'number' ? key : fallback;
|
|
532
2385
|
}
|
|
533
|
-
function
|
|
2386
|
+
function readFallbackSelectorKey(path) {
|
|
2387
|
+
const key = path[path.length - 1];
|
|
2388
|
+
return typeof key === 'string' || typeof key === 'number' ? key : 0;
|
|
2389
|
+
}
|
|
2390
|
+
function matchesSelector(value, conditions, meta) {
|
|
534
2391
|
for (const condition of conditions) {
|
|
535
|
-
if (!evaluateCondition(value, condition))
|
|
2392
|
+
if (!evaluateCondition(value, condition, meta))
|
|
536
2393
|
return false;
|
|
537
2394
|
}
|
|
538
2395
|
return true;
|
|
539
2396
|
}
|
|
540
|
-
function
|
|
2397
|
+
function applySelectorOrderingAndLimit(matches, selector) {
|
|
2398
|
+
if (selector.orderBy !== undefined) {
|
|
2399
|
+
const { path, direction } = selector.orderBy;
|
|
2400
|
+
matches.sort((left, right) => compareJsonValues(readSelectorConditionValue(left.value, path, { key: left.key, rowIndex: left.rowIndex, mapKey: left.mapKey }), readSelectorConditionValue(right.value, path, { key: right.key, rowIndex: right.rowIndex, mapKey: right.mapKey }), direction));
|
|
2401
|
+
}
|
|
2402
|
+
if (selector.limit !== undefined && matches.length > selector.limit)
|
|
2403
|
+
matches.length = selector.limit;
|
|
2404
|
+
}
|
|
2405
|
+
function compareJsonValues(left, right, direction) {
|
|
2406
|
+
const sign = direction === 'desc' ? -1 : 1;
|
|
2407
|
+
if (left === right)
|
|
2408
|
+
return 0;
|
|
2409
|
+
if (left === undefined)
|
|
2410
|
+
return 1;
|
|
2411
|
+
if (right === undefined)
|
|
2412
|
+
return -1;
|
|
2413
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
2414
|
+
return (left - right) * sign;
|
|
2415
|
+
return String(left).localeCompare(String(right)) * sign;
|
|
2416
|
+
}
|
|
2417
|
+
function makeSelectorMatchOut(match, selector) {
|
|
2418
|
+
const out = { path: match.path.slice(), row: match.key };
|
|
2419
|
+
if (selector.project !== undefined) {
|
|
2420
|
+
const projection = {};
|
|
2421
|
+
for (const field of selector.project) {
|
|
2422
|
+
const value = readPath(match.value, field);
|
|
2423
|
+
if (value !== undefined)
|
|
2424
|
+
projection[field.join('.')] = cloneJson(value);
|
|
2425
|
+
}
|
|
2426
|
+
out.projection = projection;
|
|
2427
|
+
}
|
|
2428
|
+
return out;
|
|
2429
|
+
}
|
|
2430
|
+
function evaluateCondition(value, condition, meta) {
|
|
541
2431
|
if ('and' in condition)
|
|
542
|
-
return condition.and.every((item) => evaluateCondition(value, item));
|
|
2432
|
+
return condition.and.every((item) => evaluateCondition(value, item, meta));
|
|
543
2433
|
if ('or' in condition)
|
|
544
|
-
return condition.or.some((item) => evaluateCondition(value, item));
|
|
2434
|
+
return condition.or.some((item) => evaluateCondition(value, item, meta));
|
|
545
2435
|
if ('not' in condition)
|
|
546
|
-
return !evaluateCondition(value, condition.not);
|
|
547
|
-
const actual =
|
|
2436
|
+
return !evaluateCondition(value, condition.not, meta);
|
|
2437
|
+
const actual = readSelectorConditionValue(value, normalizePath(condition.field, 'condition field'), meta);
|
|
548
2438
|
const op = normalizeOperator(condition);
|
|
549
2439
|
const expected = readExpected(condition, op);
|
|
550
2440
|
if (op === 'exists')
|
|
@@ -567,6 +2457,52 @@ function evaluateCondition(value, condition) {
|
|
|
567
2457
|
return actual <= expected;
|
|
568
2458
|
return false;
|
|
569
2459
|
}
|
|
2460
|
+
function readSelectorConditionValue(value, field, meta) {
|
|
2461
|
+
if (field.length === 1 && field[0] === '$key')
|
|
2462
|
+
return meta?.key;
|
|
2463
|
+
if (field.length === 1 && field[0] === '$index')
|
|
2464
|
+
return meta?.rowIndex;
|
|
2465
|
+
if (field.length === 1 && field[0] === '$mapKey')
|
|
2466
|
+
return meta?.mapKey;
|
|
2467
|
+
return readPath(value, field);
|
|
2468
|
+
}
|
|
2469
|
+
function isSpecialSelectorPath(path) {
|
|
2470
|
+
return path.length === 1 && (path[0] === '$key' ||
|
|
2471
|
+
path[0] === '$index' ||
|
|
2472
|
+
path[0] === '$mapKey');
|
|
2473
|
+
}
|
|
2474
|
+
function uniqueNumbers(values) {
|
|
2475
|
+
const out = [];
|
|
2476
|
+
for (const value of values) {
|
|
2477
|
+
if (!out.includes(value))
|
|
2478
|
+
out.push(value);
|
|
2479
|
+
}
|
|
2480
|
+
return out;
|
|
2481
|
+
}
|
|
2482
|
+
function uniqueStrings(values) {
|
|
2483
|
+
const out = [];
|
|
2484
|
+
for (const value of values) {
|
|
2485
|
+
if (!out.includes(value))
|
|
2486
|
+
out.push(value);
|
|
2487
|
+
}
|
|
2488
|
+
return out;
|
|
2489
|
+
}
|
|
2490
|
+
function uniqueObjectKeys(values) {
|
|
2491
|
+
const out = [];
|
|
2492
|
+
for (const value of values) {
|
|
2493
|
+
if (!out.some((existing) => Object.is(existing, value)))
|
|
2494
|
+
out.push(value);
|
|
2495
|
+
}
|
|
2496
|
+
return out;
|
|
2497
|
+
}
|
|
2498
|
+
function intersectObjectKeys(left, right) {
|
|
2499
|
+
const out = [];
|
|
2500
|
+
for (const value of left) {
|
|
2501
|
+
if (right.some((candidate) => Object.is(candidate, value)))
|
|
2502
|
+
out.push(value);
|
|
2503
|
+
}
|
|
2504
|
+
return uniqueObjectKeys(out);
|
|
2505
|
+
}
|
|
570
2506
|
function readCondition(fieldOrCondition, op, value) {
|
|
571
2507
|
if (typeof fieldOrCondition === 'string' || Array.isArray(fieldOrCondition)) {
|
|
572
2508
|
return { field: fieldOrCondition, op: op || 'eq', value };
|
|
@@ -632,8 +2568,14 @@ function normalizeOperation(op) {
|
|
|
632
2568
|
return {
|
|
633
2569
|
...op,
|
|
634
2570
|
path: op.path.slice(),
|
|
2571
|
+
to: op.to === undefined ? undefined : op.to.slice(),
|
|
2572
|
+
value: op.value === undefined ? undefined : cloneJson(op.value),
|
|
2573
|
+
expected: op.expected === undefined ? undefined : cloneJson(op.expected),
|
|
2574
|
+
values: op.values === undefined ? undefined : op.values.map((value) => cloneJson(value)),
|
|
2575
|
+
attributes: op.attributes === undefined ? undefined : cloneJson(op.attributes),
|
|
635
2576
|
repeat: normalizeIndex(op.repeat, 'mutation repeat'),
|
|
636
|
-
selector: op.selector === undefined ? undefined : cloneSelectorPlan(op.selector)
|
|
2577
|
+
selector: op.selector === undefined ? undefined : cloneSelectorPlan(op.selector),
|
|
2578
|
+
transaction: op.transaction === undefined ? undefined : cloneTransactionInfo(op.transaction)
|
|
637
2579
|
};
|
|
638
2580
|
}
|
|
639
2581
|
function isNoopRepeat(op) {
|
|
@@ -641,12 +2583,29 @@ function isNoopRepeat(op) {
|
|
|
641
2583
|
}
|
|
642
2584
|
function canUseRowFieldAssign(op) {
|
|
643
2585
|
return op.path.length > 0 && (op.kind === 'set' ||
|
|
2586
|
+
op.kind === 'upsert' ||
|
|
2587
|
+
op.kind === 'compareAndSet' ||
|
|
644
2588
|
op.kind === 'assign' ||
|
|
645
2589
|
op.kind === 'increment' ||
|
|
646
2590
|
op.kind === 'decrement' ||
|
|
2591
|
+
op.kind === 'multiply' ||
|
|
2592
|
+
op.kind === 'min' ||
|
|
2593
|
+
op.kind === 'max' ||
|
|
2594
|
+
op.kind === 'clamp' ||
|
|
647
2595
|
op.kind === 'toggle' ||
|
|
648
2596
|
op.kind === 'append' ||
|
|
2597
|
+
op.kind === 'prepend' ||
|
|
2598
|
+
op.kind === 'insert' ||
|
|
2599
|
+
op.kind === 'removeAt' ||
|
|
2600
|
+
op.kind === 'moveItem' ||
|
|
2601
|
+
op.kind === 'addToSet' ||
|
|
2602
|
+
op.kind === 'pull' ||
|
|
2603
|
+
op.kind === 'removeWhere' ||
|
|
649
2604
|
op.kind === 'appendText' ||
|
|
2605
|
+
op.kind === 'insertText' ||
|
|
2606
|
+
op.kind === 'deleteText' ||
|
|
2607
|
+
op.kind === 'replaceText' ||
|
|
2608
|
+
op.kind === 'formatText' ||
|
|
650
2609
|
op.kind === 'splice' ||
|
|
651
2610
|
op.kind === 'spliceText');
|
|
652
2611
|
}
|
|
@@ -663,6 +2622,11 @@ function normalizePath(path, label) {
|
|
|
663
2622
|
return parsePointer(path);
|
|
664
2623
|
return path.split('.').filter(Boolean);
|
|
665
2624
|
}
|
|
2625
|
+
function normalizeSelectorName(name) {
|
|
2626
|
+
if (typeof name !== 'string' || name.length === 0)
|
|
2627
|
+
throw new TypeError('selector name must be a non-empty string');
|
|
2628
|
+
return name;
|
|
2629
|
+
}
|
|
666
2630
|
function readPath(source, path) {
|
|
667
2631
|
if (source === undefined)
|
|
668
2632
|
return undefined;
|
|
@@ -673,11 +2637,22 @@ function readPath(source, path) {
|
|
|
673
2637
|
return undefined;
|
|
674
2638
|
}
|
|
675
2639
|
}
|
|
2640
|
+
function readRequiredPath(source, path) {
|
|
2641
|
+
const value = readPath(source, path);
|
|
2642
|
+
if (value === undefined)
|
|
2643
|
+
throw new TypeError('missing value at ' + JSON.stringify(path));
|
|
2644
|
+
return value;
|
|
2645
|
+
}
|
|
676
2646
|
function cloneSelectorPlan(plan) {
|
|
677
2647
|
return {
|
|
678
2648
|
path: plan.path.slice(),
|
|
679
2649
|
conditions: plan.conditions.map(cloneCondition),
|
|
680
|
-
keyBy: plan.keyBy
|
|
2650
|
+
keyBy: plan.keyBy === undefined ? undefined : plan.keyBy.slice(),
|
|
2651
|
+
indexBy: plan.indexBy === undefined ? undefined : plan.indexBy.slice(),
|
|
2652
|
+
orderBy: plan.orderBy === undefined ? undefined : { path: plan.orderBy.path.slice(), direction: plan.orderBy.direction },
|
|
2653
|
+
limit: plan.limit,
|
|
2654
|
+
project: plan.project === undefined ? undefined : plan.project.map((path) => path.slice()),
|
|
2655
|
+
name: plan.name
|
|
681
2656
|
};
|
|
682
2657
|
}
|
|
683
2658
|
function cloneCondition(condition) {
|
|
@@ -689,6 +2664,42 @@ function cloneCondition(condition) {
|
|
|
689
2664
|
return { not: cloneCondition(condition.not) };
|
|
690
2665
|
return { ...condition, field: normalizePath(condition.field, 'condition field'), in: condition.in === undefined ? undefined : condition.in.slice() };
|
|
691
2666
|
}
|
|
2667
|
+
function normalizeTransactionInfo(options) {
|
|
2668
|
+
const info = {};
|
|
2669
|
+
if (options.origin !== undefined) {
|
|
2670
|
+
if (typeof options.origin !== 'string')
|
|
2671
|
+
throw new TypeError('transaction origin must be a string');
|
|
2672
|
+
info.origin = options.origin;
|
|
2673
|
+
}
|
|
2674
|
+
if (options.metadata !== undefined) {
|
|
2675
|
+
if (!isObjectRecord(options.metadata))
|
|
2676
|
+
throw new TypeError('transaction metadata must be a JSON object');
|
|
2677
|
+
info.metadata = cloneJson(options.metadata);
|
|
2678
|
+
}
|
|
2679
|
+
if (options.timestamp !== undefined)
|
|
2680
|
+
info.timestamp = normalizeFiniteNumber(options.timestamp, 'transaction timestamp');
|
|
2681
|
+
return info;
|
|
2682
|
+
}
|
|
2683
|
+
function cloneTransactionInfo(info) {
|
|
2684
|
+
return {
|
|
2685
|
+
origin: info.origin,
|
|
2686
|
+
metadata: info.metadata === undefined ? undefined : cloneJson(info.metadata),
|
|
2687
|
+
timestamp: info.timestamp
|
|
2688
|
+
};
|
|
2689
|
+
}
|
|
2690
|
+
function cloneMutationOperation(op) {
|
|
2691
|
+
return {
|
|
2692
|
+
...op,
|
|
2693
|
+
path: op.path.slice(),
|
|
2694
|
+
to: op.to === undefined ? undefined : op.to.slice(),
|
|
2695
|
+
value: op.value === undefined ? undefined : cloneJson(op.value),
|
|
2696
|
+
expected: op.expected === undefined ? undefined : cloneJson(op.expected),
|
|
2697
|
+
values: op.values === undefined ? undefined : op.values.map((value) => cloneJson(value)),
|
|
2698
|
+
attributes: op.attributes === undefined ? undefined : cloneJson(op.attributes),
|
|
2699
|
+
selector: op.selector === undefined ? undefined : cloneSelectorPlan(op.selector),
|
|
2700
|
+
transaction: op.transaction === undefined ? undefined : cloneTransactionInfo(op.transaction)
|
|
2701
|
+
};
|
|
2702
|
+
}
|
|
692
2703
|
function internField(fields, field) {
|
|
693
2704
|
for (let i = 0; i < fields.length; i++) {
|
|
694
2705
|
if (samePath(fields[i], field))
|
|
@@ -713,11 +2724,25 @@ function normalizeInteger(value, label) {
|
|
|
713
2724
|
throw new RangeError(label + ' must be a safe integer');
|
|
714
2725
|
return value;
|
|
715
2726
|
}
|
|
2727
|
+
function normalizeFiniteNumber(value, label) {
|
|
2728
|
+
if (!Number.isFinite(value))
|
|
2729
|
+
throw new RangeError(label + ' must be a finite number');
|
|
2730
|
+
return value;
|
|
2731
|
+
}
|
|
716
2732
|
function normalizeIndex(value, label) {
|
|
717
2733
|
if (!Number.isSafeInteger(value) || value < 0)
|
|
718
2734
|
throw new RangeError(label + ' must be a non-negative safe integer');
|
|
719
2735
|
return value;
|
|
720
2736
|
}
|
|
2737
|
+
function boundedIndex(value, length) {
|
|
2738
|
+
return value > length ? length : value;
|
|
2739
|
+
}
|
|
2740
|
+
function isCodeUnitStableText(text) {
|
|
2741
|
+
return text.length === Array.from(text).length;
|
|
2742
|
+
}
|
|
2743
|
+
function pathCacheKey(path) {
|
|
2744
|
+
return JSON.stringify(path);
|
|
2745
|
+
}
|
|
721
2746
|
function compareNumbers(left, right) {
|
|
722
2747
|
return left - right;
|
|
723
2748
|
}
|
|
@@ -730,6 +2755,18 @@ function samePath(left, right) {
|
|
|
730
2755
|
}
|
|
731
2756
|
return true;
|
|
732
2757
|
}
|
|
2758
|
+
function overlappingFieldPaths(left, right) {
|
|
2759
|
+
return !samePath(left, right) && (isPathPrefix(left, right) || isPathPrefix(right, left));
|
|
2760
|
+
}
|
|
2761
|
+
function isPathPrefix(prefix, path) {
|
|
2762
|
+
if (prefix.length >= path.length)
|
|
2763
|
+
return false;
|
|
2764
|
+
for (let i = 0; i < prefix.length; i++) {
|
|
2765
|
+
if (prefix[i] !== path[i])
|
|
2766
|
+
return false;
|
|
2767
|
+
}
|
|
2768
|
+
return true;
|
|
2769
|
+
}
|
|
733
2770
|
function sameNumberArray(left, right) {
|
|
734
2771
|
if (left.length !== right.length)
|
|
735
2772
|
return false;
|
|
@@ -743,7 +2780,39 @@ function sameSelectorPlan(left, right) {
|
|
|
743
2780
|
if (left === undefined || right === undefined)
|
|
744
2781
|
return left === right;
|
|
745
2782
|
return samePath(left.path, right.path) &&
|
|
746
|
-
left.keyBy
|
|
2783
|
+
sameOptionalPath(left.keyBy, right.keyBy) &&
|
|
2784
|
+
sameOptionalPath(left.indexBy, right.indexBy) &&
|
|
2785
|
+
sameSelectorOrder(left.orderBy, right.orderBy) &&
|
|
2786
|
+
left.limit === right.limit &&
|
|
2787
|
+
sameOptionalPathList(left.project, right.project) &&
|
|
747
2788
|
JSON.stringify(left.conditions) === JSON.stringify(right.conditions);
|
|
748
2789
|
}
|
|
2790
|
+
function sameOptionalPath(left, right) {
|
|
2791
|
+
if (left === undefined || right === undefined)
|
|
2792
|
+
return left === right;
|
|
2793
|
+
return samePath(left, right);
|
|
2794
|
+
}
|
|
2795
|
+
function sameOptionalPathList(left, right) {
|
|
2796
|
+
if (left === undefined || right === undefined)
|
|
2797
|
+
return left === right;
|
|
2798
|
+
if (left.length !== right.length)
|
|
2799
|
+
return false;
|
|
2800
|
+
for (let i = 0; i < left.length; i++) {
|
|
2801
|
+
if (!samePath(left[i], right[i]))
|
|
2802
|
+
return false;
|
|
2803
|
+
}
|
|
2804
|
+
return true;
|
|
2805
|
+
}
|
|
2806
|
+
function sameSelectorOrder(left, right) {
|
|
2807
|
+
if (left === undefined || right === undefined)
|
|
2808
|
+
return left === right;
|
|
2809
|
+
return left.direction === right.direction && samePath(left.path, right.path);
|
|
2810
|
+
}
|
|
2811
|
+
function sameTransactionInfo(left, right) {
|
|
2812
|
+
if (left === undefined || right === undefined)
|
|
2813
|
+
return left === right;
|
|
2814
|
+
return left.origin === right.origin &&
|
|
2815
|
+
left.timestamp === right.timestamp &&
|
|
2816
|
+
jsonEqual(left.metadata, right.metadata);
|
|
2817
|
+
}
|
|
749
2818
|
//# sourceMappingURL=index.js.map
|