{"record":{"id":"be1a0b2f5c4064c8","repo":"overleaf/overleaf","slug":"the-transformed-op-is-invalid","errorCode":null,"errorMessage":"The transformed op is invalid","messagePattern":"The transformed op is invalid","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"services/document-updater/app/js/sharejs/types/text-tp2.js","lineNumber":364,"sourceCode":"      // Insert text or tombs\n      if (goForwards) {\n        // transform - insert skips over inserted parts\n        if (side === 'left') {\n          // The left insert should go first.\n          while (__guard__(peek(), x => x.i) !== undefined) {\n            append(newOp, take())\n          }\n        }\n\n        // In any case, skip the inserted text.\n        append(newOp, length)\n      } else {\n        // Prune. Remove skips for inserts.\n        while (length > 0) {\n          chunk = take(length, true)\n\n          if (chunk === null) {\n            throw new Error('The transformed op is invalid')\n          }\n          if (chunk.d !== undefined) {\n            throw new Error(\n              'The transformed op deletes locally inserted characters - it cannot be purged of the insert.'\n            )\n          }\n\n          if (typeof chunk === 'number') {\n            length -= chunk\n          } else {\n            append(newOp, chunk)\n          }\n        }\n      }\n    } else {\n      // Skip or delete\n      while (length > 0) {\n        chunk = take(length, true)","sourceCodeStart":346,"sourceCodeEnd":382,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/document-updater/app/js/sharejs/types/text-tp2.js#L346-L382","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nconst original = type.prune(incomingOp, historyOp)\n// after\nif (!opsAlignWithHistory(incomingOp, historyOp)) {\n  return reloadAndRebuild(incomingOp)\n}\nconst original = type.prune(incomingOp, historyOp)","handlingStrategy":"try-catch","validationCode":"if (opTotalLength(op1) > opTotalLength(op2) + docLength(op2)) {\n  return rebuildOpFromSnapshot(op1)\n}","typeGuard":"function isPrunablePair(op1, op2) {\n  return Array.isArray(op1) && Array.isArray(op2) &&\n    opLength(op1) <= opLength(op2)\n}","tryCatchPattern":"try {\n  original = tp2type.prune(op, otherOp)\n} catch (e) {\n  if (e.message === 'The transformed op is invalid') {\n    return rebuildFromHistory(op.docId, op.version) // regenerate the pre-transform op\n  }\n  throw e\n}","preventionTips":["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."],"tags":["ot","transform","prune","document-updater"],"backgroundTag":"invalid-transformed-operation","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}