@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/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.ops.push(this.currentSelector === undefined ? op : { ...op, selector: cloneSelectorPlan(this.currentSelector) });
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 rawOp of plan.operations) {
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({ path: match.path.slice(), row: match.key });
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
- if (Array.isArray(context.collection) &&
149
- context.tailPath.length === 0 &&
150
- canUseRowFieldAssign(op)) {
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
- dirtyPaths.push(absolutePath);
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 patchStart = patch.length;
170
- emitAbsoluteMutation(working, op.path, op, patch, lowered, warnings);
171
- working = applyPatchOperationsToWorking(working, patch, patchStart);
172
- dirtyPaths.push(op.path.slice());
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 = compactCrdtMutationOperations(plan.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
- applyCrdtOperation(tx, working, op.path, op);
199
- working = applyMaterializedOperationToWorking(working, op.path, op);
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
- applyCrdtOperation(tx, working, absolutePath, op);
206
- working = applyMaterializedOperationToWorking(working, absolutePath, op);
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
- return { ...compiled, commit };
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
- const nextValue = applyFieldMutation(readPath(working, path), op);
220
- return applyPatch(working, [[OP_SET, path.slice(), cloneJson(nextValue)]]);
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 compactCrdtMutationOperations(operations) {
253
- const compacted = [];
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 previous = compacted[compacted.length - 1];
259
- if (previous !== undefined && canMergeArithmeticOperations(previous, op)) {
260
- const merged = mergeArithmeticOperations(previous, op);
261
- if (merged === undefined)
262
- compacted.pop();
263
- else
264
- compacted[compacted.length - 1] = merged;
265
- continue;
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
- compacted.push(op);
1071
+ optimized.push(optimizedOp);
268
1072
  }
269
- return compacted;
1073
+ return optimized;
1074
+ }
1075
+ function withMatchWeight(op, matchWeight) {
1076
+ return { ...op, matchWeight };
270
1077
  }
271
- function canMergeArithmeticOperations(left, right) {
272
- return isArithmeticOperation(left) &&
273
- isArithmeticOperation(right) &&
274
- samePath(left.path, right.path) &&
275
- sameSelectorPlan(left.selector, right.selector);
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 emitAbsoluteMutation(source, path, op, patch, lowered, warnings) {
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 === 'appendText') {
344
- const text = (op.text || '').repeat(op.repeat);
345
- if (typeof current === 'string') {
346
- patch.push([OP_STRING_SPLICE, path.slice(), current.length, 0, text]);
347
- lowered.push('appendText');
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(), text]);
351
- lowered.push('appendText-to-set');
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 === 'splice' || op.kind === 'spliceText') {
356
- const target = applyMutationToValue(current, op, warnings);
357
- patch.push([OP_SET, path.slice(), target]);
358
- lowered.push(op.kind + '-materialized');
359
- return;
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 (op.kind === 'set') {
443
- tx.set(path, cloneJson(op.value));
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
- else if (op.kind === 'assign') {
446
- const current = readPath(state, path);
447
- tx.set(path, isObjectRecord(current) ? { ...current, ...cloneJson(op.value) } : cloneJson(op.value));
1498
+ if (decision.strategy === 'native-text' || decision.strategy === 'native-text-splice') {
1499
+ applyNativeTextCrdtOperation(tx, state, path, op);
1500
+ return;
448
1501
  }
449
- else if (op.kind === 'increment') {
450
- tx.counter(path).increment((op.delta || 0) * op.repeat);
1502
+ if (decision.strategy === 'native-list') {
1503
+ applyNativeListCrdtOperation(tx, state, path, op);
1504
+ return;
451
1505
  }
452
- else if (op.kind === 'decrement') {
453
- tx.counter(path).decrement((op.delta || 0) * op.repeat);
1506
+ if (decision.strategy === 'native-map-field') {
1507
+ applyNativeMapFieldCrdtOperation(tx, state, path, op);
1508
+ return;
454
1509
  }
455
- else if (op.kind === 'toggle') {
456
- if (op.repeat % 2 === 1)
457
- tx.set(path, !Boolean(readPath(state, path)));
1510
+ if (decision.strategy === 'native-delete') {
1511
+ tx.delete(path);
1512
+ return;
458
1513
  }
459
- else if (op.kind === 'appendText') {
460
- const current = readPath(state, path);
461
- const index = typeof current === 'string' ? Array.from(current).length : 0;
462
- tx.text(path).insert(index, (op.text || '').repeat(op.repeat));
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
- else if (op.kind === 'append') {
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
- const index = Array.isArray(current) ? current.length : 0;
467
- tx.list(path).insert(index, repeatValues(op.values || [], op.repeat));
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
- tx.set(path, applyMutationToValue(readPath(state, path), op, []));
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 resolveSelector(source, selector) {
497
- const wildcard = selector.path.indexOf('*');
498
- if (wildcard === -1)
499
- throw new TypeError('selector path must contain one * segment');
500
- if (selector.path.indexOf('*', wildcard + 1) !== -1) {
501
- throw new TypeError('selector path currently supports one * segment');
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
- const basePath = selector.path.slice(0, wildcard);
504
- const tailPath = selector.path.slice(wildcard + 1);
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
- for (let i = 0; i < collection.length; i++) {
509
- const row = collection[i];
510
- const value = tailPath.length === 0 ? row : readPath(row, tailPath);
511
- if (matchesSelector(value, selector.conditions)) {
512
- matches.push({ key: readSelectorMatchKey(row, selector, i), rowIndex: i, value, path: basePath.concat(i).concat(tailPath) });
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
- else if (isObjectRecord(collection)) {
517
- for (const key of Object.keys(collection)) {
518
- const row = collection[key];
519
- const value = tailPath.length === 0 ? row : readPath(row, tailPath);
520
- if (matchesSelector(value, selector.conditions)) {
521
- matches.push({ key: readSelectorMatchKey(row, selector, key), value, path: basePath.concat(key).concat(tailPath) });
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
- return { selector, basePath, tailPath, collection: collection, matches };
2313
+ state.usedIndex = true;
2314
+ return out;
526
2315
  }
527
- function readSelectorMatchKey(row, selector, fallback) {
528
- if (selector.keyBy === undefined)
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 = readPath(row, [selector.keyBy]);
2383
+ const key = readSelectorConditionValue(row, keyPath, meta);
531
2384
  return typeof key === 'string' || typeof key === 'number' ? key : fallback;
532
2385
  }
533
- function matchesSelector(value, conditions) {
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 evaluateCondition(value, condition) {
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 = readPath(value, normalizePath(condition.field, 'condition field'));
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 === right.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