{"record":{"id":"a53be6759efefb4c","repo":"overleaf/overleaf","slug":"remaining-fragments-in-op1-component","errorCode":null,"errorMessage":"Remaining fragments in op1: ${component}","messagePattern":"Remaining fragments in op1: (.+?)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"services/document-updater/app/js/sharejs/types/text-tp2.js","lineNumber":481,"sourceCode":"          )\n        }\n\n        const chunkLength = componentLength(chunk)\n        if (chunk.i !== undefined) {\n          append(result, { i: chunkLength })\n        } else {\n          append(result, { d: chunkLength })\n        }\n\n        length -= chunkLength\n      }\n    }\n  }\n\n  // Append extras from op1\n  while ((component = take())) {\n    if (component.i === undefined) {\n      throw new Error(`Remaining fragments in op1: ${component}`)\n    }\n    append(result, component)\n  }\n\n  return result\n}\n\nif (typeof WEB !== 'undefined' && WEB !== null) {\n  exports.types['text-tp2'] = type\n} else {\n  module.exports = type\n}\n\nfunction __guard__(value, transform) {\n  return typeof value !== 'undefined' && value !== null\n    ? transform(value)\n    : undefined\n}","sourceCodeStart":463,"sourceCodeEnd":499,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/document-updater/app/js/sharejs/types/text-tp2.js#L463-L499","documentation":"After transformX consumes the document and op2, it drains whatever remains of op1 via take(). Any leftover component that is not an insert means op1 still contained skip/delete fragments the document could not satisfy — op1 is longer than the document. The library throws instead of emitting a malformed transformed op.","triggerScenarios":"transform(op1, op2) where op1 contains trailing non-insert components (string skips or {d:...} deletes) that extend past the document that op2 applies to — i.e. op1 was generated against a longer document.","commonSituations":"Ops from an older/longer document version arriving after truncation or rollback, mixing plain text ops with text-tp2 ops, or a bug in a client's op builder leaving unnormalized trailing delete/skip components.","solutions":["Confirm op1's base snapshot version matches the document being transformed; rebuild op1 from a fresh snapshot if not.","Normalize the op before transforming so trailing skips/deletes are removed when they exceed document length.","Ensure both ops are text-tp2 ops (this check lives only in the tp2 type); do not pass plain-text ops in.","Log the full op1 and document length on failure to identify which client submitted the stale op."],"exampleFix":"// before\nconst result = textTp2.transform(op1, op2, 'left')\n// after: trim non-insert trailing fragments beyond doc length\nconst docLen = doc.length\nlet pos = 0, trimmed = []\nfor (const c of op1) {\n  if (typeof c === 'string' || c.d) { const len = (typeof c === 'string' ? c : c.d).length; if (pos + len > docLen) break; pos += len }\n  trimmed.push(c)\n}\nconst result = textTp2.transform(trimmed, op2, 'left')","handlingStrategy":"validation","validationCode":"// Ensure op1's non-insert fragments fit the document\nlet pos = 0\nfor (const c of op1) {\n  const len = typeof c === 'string' ? c.length : (c.d !== undefined ? c.d.length : 0)\n  pos += len\n}\nif (pos > doc.length) throw new Error('op1 skips/deletes beyond document length')","typeGuard":null,"tryCatchPattern":"try {\n  transformed = textTp2.transform(op1, op2, 'left')\n} catch (e) {\n  if (String(e.message).startsWith('Remaining fragments in op1')) {\n    // op1 is stale/overlong: discard and rebuild from current snapshot\n    return rebuildOpFromSnapshot()\n  }\n  throw e\n}","preventionTips":["Track the base version each op was built against and reject ops from mismatched versions","Normalize ops before transform so trailing garbage fragments are removed","Avoid mixing text and text-tp2 ops in the same document","Persist doc version alongside ops so replay/rebase can detect staleness"],"tags":["ot","transform","document-updater","stale-op"],"backgroundTag":"op-traverses-past-document-end","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}