overleaf/overleaf · critical · Error

The transformed op is invalid

Error message

The transformed op is invalid

What it means

During the prune direction of transformer(), the code walks op2 (the other op) while consuming op1's components. If take() runs dry - op1 ended before the required length was consumed - the result would be a malformed transformed op, so it throws. This means the op being pruned is not consistent with the document/op it is pruned against.

Source

Thrown at services/document-updater/app/js/sharejs/types/text-tp2.js:364

      // Insert text or tombs
      if (goForwards) {
        // transform - insert skips over inserted parts
        if (side === 'left') {
          // The left insert should go first.
          while (__guard__(peek(), x => x.i) !== undefined) {
            append(newOp, take())
          }
        }

        // In any case, skip the inserted text.
        append(newOp, length)
      } else {
        // Prune. Remove skips for inserts.
        while (length > 0) {
          chunk = take(length, true)

          if (chunk === null) {
            throw new Error('The transformed op is invalid')
          }
          if (chunk.d !== undefined) {
            throw new Error(
              'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'
            )
          }

          if (typeof chunk === 'number') {
            length -= chunk
          } else {
            append(newOp, chunk)
          }
        }
      }
    } else {
      // Skip or delete
      while (length > 0) {
        chunk = take(length, true)

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Verify op1 and op2 apply to the same document state and that op2's effects precede op1's in history.
  2. Rebuild the op chain from a fresh snapshot rather than pruning a stale/corrupt op.
  3. Pre-validate that the combined skip+delete length of the op fits within the document length.
  4. Log and reject the op in the update pipeline before it reaches the transformer.

Example fix

// before
const original = type.prune(incomingOp, historyOp)
// after
if (!opsAlignWithHistory(incomingOp, historyOp)) {
  return reloadAndRebuild(incomingOp)
}
const original = type.prune(incomingOp, historyOp)
Defensive patterns

Strategy: try-catch

Validate before calling

if (opTotalLength(op1) > opTotalLength(op2) + docLength(op2)) {
  return rebuildOpFromSnapshot(op1)
}

Type guard

function isPrunablePair(op1, op2) {
  return Array.isArray(op1) && Array.isArray(op2) &&
    opLength(op1) <= opLength(op2)
}

Try / catch

try {
  original = tp2type.prune(op, otherOp)
} catch (e) {
  if (e.message === 'The transformed op is invalid') {
    return rebuildFromHistory(op.docId, op.version) // regenerate the pre-transform op
  }
  throw e
}

Prevention

When it happens

Trigger: Calling type.prune(op1, op2) where op1's skip/delete length exceeds what op2 (and the document) covers; pruning an op that was already transformed or belongs to a different document version; passing null/undefined-derived ops in.

Common situations: Undo/redo implementations pruning against the wrong inverse op; op history stored incorrectly so pruning skips past the end; replaying ops after a partial history reload in document-updater.

Related errors


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/be1a0b2f5c4064c8. Report an issue: GitHub.