Class: Scarpe::Webview::WebWrangler::DOMWrangler

Inherits:
Object
  • Object
show all
Includes:
Shoes::Log
Defined in:
lib/scarpe/wv/web_wrangler.rb

Overview

Leaving DOM changes as "meh, async, we'll see when it happens" is terrible for testing. Instead, we need to track whether particular changes have committed yet or not. So we add a single gateway for all DOM changes, and we make sure its work is done before we consider a redraw complete.

DOMWrangler batches up changes into fewer RPC calls. It's fine to have a redraw "in flight" and have changes waiting to catch the next bus. But we don't want more than one in flight, since it seems like having too many pending RPC requests can crash Webview. So we allow one redraw scheduled and one redraw promise waiting, at maximum.

A WebWrangler will create and wrap a DOMWrangler, serving as the interface for all DOM operations.

A batch of DOMWrangler changes may be removed if a full update is scheduled. That update is considered to replace the previous incremental changes. Any changes that need to execute even if a full update happens should be scheduled through WebWrangler#eval_js_async, not DOMWrangler.

Constant Summary

Constants included from Shoes::Log

Shoes::Log::DEFAULT_COMPONENT, Shoes::Log::DEFAULT_DEBUG_LOG_CONFIG, Shoes::Log::DEFAULT_LOG_CONFIG

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Shoes::Log

configure_logger, #log_init, logger

Constructor Details

#initialize(web_wrangler) ⇒ DOMWrangler

Create a DOMWrangler that is paired with a WebWrangler. The WebWrangler is treated as an underlying abstraction for reliable JS evaluation.



541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
# File 'lib/scarpe/wv/web_wrangler.rb', line 541

def initialize(web_wrangler)
  log_init("Webview::WebWrangler::DOMWrangler")

  @wrangler = web_wrangler

  @waiting_changes = []
  @pending_redraw_promise = nil
  @waiting_redraw_promise = nil

  @fully_up_to_date_promise = nil

  # Initially we're waiting for a full replacement to happen.
  # It's possible to request updates/changes before we have
  # a DOM in place and before Webview is running. If we do
  # that, we should discard those updates.
  @first_draw_requested = false

  @redraw_handlers = []

  # The "fully up to date" logic is complicated and not
  # as well tested as I'd like. This makes it far less
  # likely that the event simply won't fire.
  # With more comprehensive testing, this should be
  # removable.
  web_wrangler.periodic_code("scarpeDOMWranglerHeartbeat") do
    if @fully_up_to_date_promise && fully_updated?
      @log.info("Fulfilling up-to-date promise on heartbeat")
      @fully_up_to_date_promise.fulfilled!
      @fully_up_to_date_promise = nil
    end
  end
end

Instance Attribute Details

#pending_redraw_promiseObject (readonly)

A Scarpe::Promise for JS that has been scheduled to execute but is not yet verified complete



531
532
533
# File 'lib/scarpe/wv/web_wrangler.rb', line 531

def pending_redraw_promise
  @pending_redraw_promise
end

#waiting_changesObject (readonly)

Changes that have not yet been executed



528
529
530
# File 'lib/scarpe/wv/web_wrangler.rb', line 528

def waiting_changes
  @waiting_changes
end

#waiting_redraw_promiseObject (readonly)

A Scarpe::Promise for waiting changes - it will be fulfilled when all waiting changes have been verified complete, or when a full redraw that removed them has been verified complete. If many small changes are scheduled, the same promise will be returned for many of them.



537
538
539
# File 'lib/scarpe/wv/web_wrangler.rb', line 537

def waiting_redraw_promise
  @waiting_redraw_promise
end

Class Method Details

.replacement_code(html_text) ⇒ Object



583
584
585
# File 'lib/scarpe/wv/web_wrangler.rb', line 583

def self.replacement_code(html_text)
  "document.getElementById('wrapper-wvroot').innerHTML = `#{html_text}`; true"
end

Instance Method Details

#fully_updated?Boolean

Returns:

  • (Boolean)


702
703
704
# File 'lib/scarpe/wv/web_wrangler.rb', line 702

def fully_updated?
  @pending_redraw_promise.nil? && @waiting_redraw_promise.nil? && @waiting_changes.empty?
end

#on_every_redraw(&block) ⇒ Object



596
597
598
# File 'lib/scarpe/wv/web_wrangler.rb', line 596

def on_every_redraw(&block)
  @redraw_handlers << block
end

#promise_fully_updatedObject

Return a promise which will be fulfilled when the DOM is fully up-to-date



707
708
709
710
711
712
713
714
715
716
717
718
719
720
# File 'lib/scarpe/wv/web_wrangler.rb', line 707

def promise_fully_updated
  if fully_updated?
    # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
    return ::Scarpe::Promise.fulfilled
  end

  # Do we already have a promise for this? Return it. Everybody can share one.
  if @fully_up_to_date_promise
    return @fully_up_to_date_promise
  end

  # We're not fully updated, so we need a promise. Create it, return it.
  @fully_up_to_date_promise = ::Scarpe::Promise.new
end

#promise_redrawObject

promise_redraw returns a Scarpe::Promise which will be fulfilled after all current pending or waiting changes have completed. This may require creating a new promise.

What are the states of redraw? "empty" - no waiting promise, no pending-redraw promise, no pending changes "pending only" - no waiting promise, but we have a pending redraw with some changes; it hasn't committed yet "pending and waiting" - we have a waiting promise for our unscheduled changes; we can add more unscheduled changes since we haven't scheduled them yet.

This is often called after adding a new waiting change or replacing them, so the state may have just changed. It can also be called when no changes have been made and no updates need to happen.



612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
# File 'lib/scarpe/wv/web_wrangler.rb', line 612

def promise_redraw
  if fully_updated?
    # No changes to make, nothing in-process or waiting, so just return a pre-fulfilled promise
    @log.debug("Requesting redraw but there are no pending changes or promises, return pre-fulfilled")
    return ::Scarpe::Promise.fulfilled
  end

  # Already have a redraw requested *and* one on deck? Then all current changes will have committed
  # when we (eventually) fulfill the waiting_redraw_promise.
  if @waiting_redraw_promise
    @log.debug("Promising eventual redraw of #{@waiting_changes.size} waiting unscheduled changes.")
    return @waiting_redraw_promise
  end

  if @waiting_changes.empty?
    # There's no waiting_redraw_promise. There are no waiting changes. But we're not fully updated.
    # So there must be a redraw in flight, and we don't need to schedule a new waiting_redraw_promise.
    @log.debug("Returning in-flight redraw promise")
    return @pending_redraw_promise
  end

  @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes and no waiting promise - need to schedule something!")

  # We have at least one waiting change, possibly newly-added. We have no waiting_redraw_promise.
  # Do we already have a redraw in-flight?
  if @pending_redraw_promise
    # Yes we do. Schedule a new waiting promise. When it turns into the pending_redraw_promise it will
    # grab all waiting changes. In the mean time, it sits here and waits.
    #
    # We *could* do a fancy promise thing and have it update @waiting_changes for itself, etc, when it
    # schedules itself. But we should always be calling promise_redraw or having a redraw fulfilled (see below)
    # when these things change. I'd rather keep the logic in this method. It's easier to reason through
    # all the cases.
    @waiting_redraw_promise = ::Scarpe::Promise.new

    @log.debug("Creating a new waiting promise since a pending promise is already in place")
    return @waiting_redraw_promise
  end

  # We have no redraw in-flight and no pre-existing waiting line. The new change(s) are presumably right
  # after things were fully up-to-date. We can schedule them for immediate redraw.

  @log.debug("Requesting redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!")
  promise = schedule_waiting_changes # This clears the waiting changes
  @pending_redraw_promise = promise

  promise.on_fulfilled do
    @redraw_handlers.each(&:call)
    @pending_redraw_promise = nil

    if @waiting_redraw_promise
      # While this redraw was in flight, more waiting changes got added and we made a promise
      # about when they'd complete. Now they get scheduled, and we'll fulfill the waiting
      # promise when that redraw finishes. Clear the old waiting promise. We'll add a new one
      # when/if more changes are scheduled during this redraw.
      old_waiting_promise = @waiting_redraw_promise
      @waiting_redraw_promise = nil

      @log.debug "Fulfilled redraw with #{@waiting_changes.size} waiting changes - scheduling a new redraw for them!"

      new_promise = promise_redraw
      new_promise.on_fulfilled { old_waiting_promise.fulfilled! }
    else
      # The in-flight redraw completed, and there's still no waiting promise. Good! That means
      # we should be fully up-to-date.
      @log.debug "Fulfilled redraw with no waiting changes - marking us as up to date!"
      if @waiting_changes.empty?
        # We're fully up to date! Fulfill the promise. Now we don't need it again until somebody asks
        # us for another.
        if @fully_up_to_date_promise
          @fully_up_to_date_promise.fulfilled!
          @fully_up_to_date_promise = nil
        end
      else
        @log.error "WHOAH, WHAT? My logic must be wrong, because there's " +
          "no waiting promise, but waiting changes!"
      end
    end

    @log.debug("Redraw is now fully up-to-date") if fully_updated?
  end.on_rejected do
    @log.error "Could not complete JS redraw! #{promise.reason.full_message}"
    @log.debug("REDRAW FULLY UP TO DATE BUT JS FAILED") if fully_updated?

    raise Scarpe::JSRedrawError, "JS Redraw failed! Bailing!"

    # Later we should figure out how to handle this. Clear the promises and queues and request another redraw?
  end
end

#request_change(js_code) ⇒ Object



574
575
576
577
578
579
580
581
# File 'lib/scarpe/wv/web_wrangler.rb', line 574

def request_change(js_code)
  # No updates until there's something to update
  return unless @first_draw_requested

  @waiting_changes << js_code

  promise_redraw
end

#request_replace(html_text) ⇒ Object



587
588
589
590
591
592
593
594
# File 'lib/scarpe/wv/web_wrangler.rb', line 587

def request_replace(html_text)
  # Replace other pending changes, they're not needed any more
  @waiting_changes = [DOMWrangler.replacement_code(html_text)]
  @first_draw_requested = true

  @log.debug("Requesting DOM replacement...")
  promise_redraw
end