readUpdateV2 = (decoder, ydoc, transactionOrigin, structDecoder = new UpdateDecoderV2(decoder)) => transact(ydoc, transaction => {
  // force that transaction.local is set to non-local
  transaction.local = false;
  let retry = false;
  const doc = transaction.doc;
  const store = doc.store; // let start = performance.now()

  const ss = readClientsStructRefs(structDecoder, doc); // console.log('time to read structs: ', performance.now() - start) // @todo remove
  // start = performance.now()
  // console.log('time to merge: ', performance.now() - start) // @todo remove
  // start = performance.now()

  const restStructs = integrateStructs(transaction, store, ss);
  const pending = store.pendingStructs;

  if (pending) {
    // check if we can apply something
    for (const [client, clock] of pending.missing) {
      if (clock < getState(store, client)) {
        retry = true;
        break;
      }
    }

    if (restStructs) {
      // merge restStructs into store.pending
      for (const [client, clock] of restStructs.missing) {
        const mclock = pending.missing.get(client);

        if (mclock == null || mclock > clock) {
          pending.missing.set(client, clock);
        }
      }

      pending.update = mergeUpdatesV2([pending.update, restStructs.update]);
    }
  } else {
    store.pendingStructs = restStructs;
  } // console.log('time to integrate: ', performance.now() - start) // @todo remove
  // start = performance.now()


  const dsRest = readAndApplyDeleteSet(structDecoder, transaction, store);

  if (store.pendingDs) {
    // @todo we could make a lower-bound state-vector check as we do above
    const pendingDSUpdate = new UpdateDecoderV2(decoding.createDecoder(store.pendingDs));
    decoding.readVarUint(pendingDSUpdate.restDecoder); // read 0 structs, because we only encode deletes in pendingdsupdate

    const dsRest2 = readAndApplyDeleteSet(pendingDSUpdate, transaction, store);

    if (dsRest && dsRest2) {
      // case 1: ds1 != null && ds2 != null
      store.pendingDs = mergeUpdatesV2([dsRest, dsRest2]);
    } else {
      // case 2: ds1 != null
      // case 3: ds2 != null
      // case 4: ds1 == null && ds2 == null
      store.pendingDs = dsRest || dsRest2;
    }
  } else {
    // Either dsRest == null && pendingDs == null OR dsRest != null
    store.pendingDs = dsRest;
  } // console.log('time to cleanup: ', performance.now() - start) // @todo remove
  // start = performance.now()
  // console.log('time to resume delete readers: ', performance.now() - start) // @todo remove
  // start = performance.now()


  if (retry) {
    const update =
    /** @type {{update: Uint8Array}} */
    store.pendingStructs.update;
    store.pendingStructs = null;
    applyUpdateV2(transaction.doc, update);
  }
}, transactionOrigin, false)
Read and apply a document update.

This function has the same effect as `applyUpdate` but accepts an decoder.

@param {decoding.Decoder} decoder
@param {Doc} ydoc
@param {any} [transactionOrigin] This will be stored on `transaction.origin` and `.on('update', (update, origin))`
@param {UpdateDecoderV1 | UpdateDecoderV2} [structDecoder]

@function