overleaf/overleaf · error · Error

Remaining fragments in op1: ${component}

Error message

Remaining fragments in op1: ${component}

What it means

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.

Source

Thrown at services/document-updater/app/js/sharejs/types/text-tp2.js:481

          )
        }

        const chunkLength = componentLength(chunk)
        if (chunk.i !== undefined) {
          append(result, { i: chunkLength })
        } else {
          append(result, { d: chunkLength })
        }

        length -= chunkLength
      }
    }
  }

  // Append extras from op1
  while ((component = take())) {
    if (component.i === undefined) {
      throw new Error(`Remaining fragments in op1: ${component}`)
    }
    append(result, component)
  }

  return result
}

if (typeof WEB !== 'undefined' && WEB !== null) {
  exports.types['text-tp2'] = type
} else {
  module.exports = type
}

function __guard__(value, transform) {
  return typeof value !== 'undefined' && value !== null
    ? transform(value)
    : undefined
}

View on GitHub (pinned to 28ad3b03b7)

Solutions

  1. Confirm op1's base snapshot version matches the document being transformed; rebuild op1 from a fresh snapshot if not.
  2. Normalize the op before transforming so trailing skips/deletes are removed when they exceed document length.
  3. Ensure both ops are text-tp2 ops (this check lives only in the tp2 type); do not pass plain-text ops in.
  4. Log the full op1 and document length on failure to identify which client submitted the stale op.

Example fix

// before
const result = textTp2.transform(op1, op2, 'left')
// after: trim non-insert trailing fragments beyond doc length
const docLen = doc.length
let pos = 0, trimmed = []
for (const c of op1) {
  if (typeof c === 'string' || c.d) { const len = (typeof c === 'string' ? c : c.d).length; if (pos + len > docLen) break; pos += len }
  trimmed.push(c)
}
const result = textTp2.transform(trimmed, op2, 'left')
Defensive patterns

Strategy: validation

Validate before calling

// Ensure op1's non-insert fragments fit the document
let pos = 0
for (const c of op1) {
  const len = typeof c === 'string' ? c.length : (c.d !== undefined ? c.d.length : 0)
  pos += len
}
if (pos > doc.length) throw new Error('op1 skips/deletes beyond document length')

Try / catch

try {
  transformed = textTp2.transform(op1, op2, 'left')
} catch (e) {
  if (String(e.message).startsWith('Remaining fragments in op1')) {
    // op1 is stale/overlong: discard and rebuild from current snapshot
    return rebuildOpFromSnapshot()
  }
  throw e
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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