overleaf/overleaf · error · Error
unknown op type
Error message
unknown op type
What it means
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).
Source
Thrown at services/document-updater/app/js/sharejs/types/text.js:207
return pos + c.i.length
} else {
return pos
}
} else if (c.d != null) {
// I think this could also be written as: Math.min(c.p, Math.min(c.p - otherC.p, otherC.d.length))
// but I think its harder to read that way, and it compiles using ternary operators anyway
// so its no slower written like this.
if (pos <= c.p) {
return pos
} else if (pos <= c.p + c.d.length) {
return c.p
} else {
return pos - c.d.length
}
} else if (c.c != null) {
return pos
} else {
throw new Error('unknown op type')
}
}
// Helper method to transform a cursor position as a result of an op.
//
// Like transformPosition above, if c is an insert, insertAfter specifies whether the cursor position
// is pushed after an insert (true) or before it (false).
text.transformCursor = function (position, op, side) {
const insertAfter = side === 'right'
for (const c of Array.from(op)) {
position = transformPosition(position, c, insertAfter)
}
return position
}
// Transform an op component by another op component. Asymmetric.
// The result will be appended to destination.
//View on GitHub (pinned to 28ad3b03b7)
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
Example fix
// before newPos = text.transformCursor(cursorPos, otherOps) // after const safeOps = otherOps.filter(c => c.i != null || c.d != null || c.c != null) newPos = text.transformCursor(cursorPos, safeOps)
Defensive patterns
Strategy: validation
Validate before calling
const safeOps = ops.filter(c =>
c.i != null || c.d != null || c.c != null
)
if (safeOps.length !== ops.length) {
log.warn('dropped malformed components before cursor transform')
}
const newPos = text.transformCursor(pos, safeOps) Type guard
function isTransformableComponent(c) {
return c.i != null || c.d != null || c.c != null
} Try / catch
try {
newPos = text.transformCursor(cursorPos, otherOps)
} catch (e) {
if (e.message === 'unknown op type') {
newPos = cursorPos // keep cursor; skip malformed op
} else {
throw e
}
} Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- invalid / missing instruction in op
- must be a string?
- The op traverses more elements than the document has
- Remaining fragments in the op: ${i(component)}
- Unknown op type
AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03).
Data as JSON: /api/errors/728ec6ac3fe9ce48.
Report an issue: GitHub.