{"record":{"id":"728ec6ac3fe9ce48","repo":"overleaf/overleaf","slug":"unknown-op-type-728ec6","errorCode":null,"errorMessage":"unknown op type","messagePattern":"unknown op type","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"services/document-updater/app/js/sharejs/types/text.js","lineNumber":207,"sourceCode":"      return pos + c.i.length\n    } else {\n      return pos\n    }\n  } else if (c.d != null) {\n    // I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))\n    // but I think its harder to read that way, and it compiles using ternary operators anyway\n    // so its no slower written like this.\n    if (pos <= c.p) {\n      return pos\n    } else if (pos <= c.p + c.d.length) {\n      return c.p\n    } else {\n      return pos - c.d.length\n    }\n  } else if (c.c != null) {\n    return pos\n  } else {\n    throw new Error('unknown op type')\n  }\n}\n\n// Helper method to transform a cursor position as a result of an op.\n//\n// Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position\n// is pushed after an insert (true) or before it (false).\ntext.transformCursor = function (position, op, side) {\n  const insertAfter = side === 'right'\n  for (const c of Array.from(op)) {\n    position = transformPosition(position, c, insertAfter)\n  }\n  return position\n}\n\n// Transform an op component by another op component. Asymmetric.\n// The result will be appended to destination.\n//","sourceCodeStart":189,"sourceCodeEnd":225,"githubUrl":"https://github.com/overleaf/overleaf/blob/28ad3b03b71cb4311decdcb55c36b33ec10d72db/services/document-updater/app/js/sharejs/types/text.js#L189-L225","documentation":"transformPosition maps a cursor position through a single op component. This fork supports three component kinds: insert ({i}), delete ({d}), and comment ({c}, which leaves the position unchanged). If a component has none of these, transformPosition cannot compute the new position and throws 'unknown op type' (lowercase variant of the apply-side error).","triggerScenarios":"Calling transformPosition(pos, c) (via text.transformCursor or text.transform) with a component c lacking i/d/c fields; the same malformed-op root cause as the apply-side 'Unknown op type', surfacing during cursor transformation of concurrent ops.","commonSituations":"Real-time cursors/presence fed ops from a client using a different op schema; deserialized legacy or corrupted ops routed through transform; custom code composing transforms with hand-built placeholder components.","solutions":["Ensure every component passed to transform/transformCursor has exactly one string i, d, or c field","Validate ops (checkValidOp) before transforming cursors or applying them","Version-gate op producers and consumers so only known component schemas reach the OT layer","Wrap cursor transformation in a guard that skips components with no recognized field instead of crashing presence updates"],"exampleFix":"// before\nnewPos = text.transformCursor(cursorPos, otherOps)\n// after\nconst safeOps = otherOps.filter(c => c.i != null || c.d != null || c.c != null)\nnewPos = text.transformCursor(cursorPos, safeOps)","handlingStrategy":"validation","validationCode":"const safeOps = ops.filter(c =>\n  c.i != null || c.d != null || c.c != null\n)\nif (safeOps.length !== ops.length) {\n  log.warn('dropped malformed components before cursor transform')\n}\nconst newPos = text.transformCursor(pos, safeOps)","typeGuard":"function isTransformableComponent(c) {\n  return c.i != null || c.d != null || c.c != null\n}","tryCatchPattern":"try {\n  newPos = text.transformCursor(cursorPos, otherOps)\n} catch (e) {\n  if (e.message === 'unknown op type') {\n    newPos = cursorPos // keep cursor; skip malformed op\n  } else {\n    throw e\n  }\n}","preventionTips":["Reuse the same component validation for transform paths as for apply paths","Filter presence/cursor ops from untrusted clients before transforming","Keep op producer and consumer type definitions in a shared schema/module","Never pass hand-built placeholder components into transform; use _append to compose ops"],"tags":["ot","sharejs","transform","cursor","malformed-op"],"backgroundTag":"ot-op-snapshot-mismatch","analyzedSha":"28ad3b03b71cb4311decdcb55c36b33ec10d72db","analyzedAt":"2026-09-03T02:10:22.807Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T07:17:11.731Z"}