Class: Scarpe::Webview::WebWrangler::DOMWrangler
- Inherits:
-
Object
- Object
- Scarpe::Webview::WebWrangler::DOMWrangler
- 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
-
#pending_redraw_promise ⇒ Object
readonly
A Scarpe::Promise for JS that has been scheduled to execute but is not yet verified complete.
-
#waiting_changes ⇒ Object
readonly
Changes that have not yet been executed.
-
#waiting_redraw_promise ⇒ Object
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.
Class Method Summary collapse
Instance Method Summary collapse
- #fully_updated? ⇒ Boolean
-
#initialize(web_wrangler) ⇒ DOMWrangler
constructor
Create a DOMWrangler that is paired with a WebWrangler.
- #on_every_redraw(&block) ⇒ Object
-
#promise_fully_updated ⇒ Object
Return a promise which will be fulfilled when the DOM is fully up-to-date.
-
#promise_redraw ⇒ Object
promise_redraw returns a Scarpe::Promise which will be fulfilled after all current pending or waiting changes have completed.
- #request_change(js_code) ⇒ Object
- #request_replace(html_text) ⇒ Object
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_promise ⇒ Object (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_changes ⇒ Object (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_promise ⇒ Object (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
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_updated ⇒ Object
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_redraw ⇒ Object
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.}" @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 |