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
- Verify op1 and op2 apply to the same document state and that op2's effects precede op1's in history.
- Rebuild the op chain from a fresh snapshot rather than pruning a stale/corrupt op.
- Pre-validate that the combined skip+delete length of the op fits within the document length.
- 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
- Store full op history per document so prune inputs are always available and in order.
- Prune only ops that were produced by a transform of the same history chain.
- Assert op lengths against the document before entering the transformer.
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
- must be a string?
- The op traverses more elements than the document has
- Remaining fragments in the op: ${i(component)}
- The transformed op deletes locally inserted characters - it
- The op traverses more elements than the document has
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/be1a0b2f5c4064c8.
Report an issue: GitHub.