All files / json-crdt-patch compaction.ts

96.29% Statements 52/54
88.88% Branches 16/18
100% Functions 2/2
100% Lines 44/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77          1x 1x                         1x 19x 19x 19x 19x 22x 22x 22x 1x 1x 1x   21x 20x 20x 20x 19x 19x 19x 19x 19x 18x 18x 18x                     1x 8x 8x 8x 8x 8x 15x 15x 7x 7x 7x 7x 7x 7x 3x 3x     12x 12x   8x    
/**
 * @description Operations for combining patches together, combining operations
 *     together, and cleaning up operations.
 */
 
import {equal, Timestamp} from './clock';
import {InsStrOp, NopOp} from './operations';
import type {JsonCrdtPatchOperation, Patch} from './Patch';
 
/**
 * Combines two or more patches together. The first patch is modified in place.
 * Operations from the second patch are appended to the first patch as is
 * (without cloning).
 *
 * The patches must have the same `sid`. The first patch must have lower logical
 * time than the second patch, and the logical times must not overlap.
 *
 * @param patches The patches to combine.
 */
export const combine = (patches: Patch[]): void => {
  const firstPatch = patches[0];
  const firstPatchId = firstPatch.getId();
  const patchesLength = patches.length;
  for (let i = 1; i < patchesLength; i++) {
    const currentPatch = patches[i];
    const currentPatchId = currentPatch.getId();
    if (!firstPatchId) {
      Iif (!currentPatchId) return;
      firstPatch.ops = firstPatch.ops.concat(currentPatch.ops);
      return;
    }
    if (!currentPatchId) return;
    Iif (!firstPatchId || !currentPatchId) throw new Error('EMPTY_PATCH');
    const sidA = firstPatchId.sid;
    if (sidA !== currentPatchId.sid) throw new Error('SID_MISMATCH');
    const timeA = firstPatchId.time;
    const nextTick = timeA + firstPatch.span();
    const timeB = currentPatchId.time;
    const timeDiff = timeB - nextTick;
    if (timeDiff < 0) throw new Error('TIMESTAMP_CONFLICT');
    const needsNoop = timeDiff > 0;
    if (needsNoop) firstPatch.ops.push(new NopOp(new Timestamp(sidA, nextTick), timeDiff));
    firstPatch.ops = firstPatch.ops.concat(currentPatch.ops);
  }
};
 
/**
 * Compacts operations within a patch. Combines consecutive string inserts.
 * Mutates the operations in place. (Use `patch.clone()` to avoid mutating the
 * original patch.)
 *
 * @param patch The patch to compact.
 */
export const compact = (patch: Patch): void => {
  const ops = patch.ops;
  const length = ops.length;
  let lastOp: JsonCrdtPatchOperation = ops[0];
  const newOps: JsonCrdtPatchOperation[] = [lastOp];
  for (let i = 1; i < length; i++) {
    const op = ops[i];
    if (lastOp instanceof InsStrOp && op instanceof InsStrOp) {
      const lastOpNextTick = lastOp.id.time + lastOp.span();
      const isTimeConsecutive = lastOpNextTick === op.id.time;
      const isInsertIntoSameString = equal(lastOp.obj, op.obj);
      const opRef = op.ref;
      const isAppend = lastOpNextTick === opRef.time + 1 && lastOp.ref.sid === opRef.sid;
      if (isTimeConsecutive && isInsertIntoSameString && isAppend) {
        lastOp.data = lastOp.data + op.data;
        continue;
      }
    }
    newOps.push(op);
    lastOp = op;
  }
  patch.ops = newOps;
};