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

  1. Ensure every component passed to transform/transformCursor has exactly one string i, d, or c field
  2. Validate ops (checkValidOp) before transforming cursors or applying them
  3. Version-gate op producers and consumers so only known component schemas reach the OT layer
  4. 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

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


AI-assisted analysis of overleaf/overleaf@28ad3b03b7 (2026-09-03). Data as JSON: /api/errors/728ec6ac3fe9ce48. Report an issue: GitHub.