Skip to content

Commit

Permalink
runtime: put white gloves on when touching clojure.browser.repl/boots…
Browse files Browse the repository at this point in the history
…trap

@bendlas
  • Loading branch information
darwin committed Sep 30, 2016
1 parent 4343aac commit e5f42d3
Show file tree
Hide file tree
Showing 5 changed files with 74 additions and 12 deletions.
6 changes: 3 additions & 3 deletions src/implant/dirac/implant/eval.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -135,8 +135,8 @@

; -- code templates ---------------------------------------------------------------------------------------------------------

(defn installation-test-template []
(str "dirac.runtime.installed_QMARK_()"))
(defn ^:dynamic installation-test-template []
(str "(dirac.runtime.installed_QMARK_() && dirac.runtime.repl.bootstrapped_QMARK_())"))

(defn ^:dynamic output-template [job-id kind text]
(str "dirac.runtime.repl.present_output(" job-id ", '" kind "', " (code-as-string text) ")"))
Expand All @@ -148,7 +148,7 @@
" dirac.runtime.repl.postprocess_unsuccessful_eval(e)"
"}"))

(defn console-log-template [method & args]
(defn ^:dynamic console-log-template [method & args]
(str "console." method "(" (string/join (interpose "," (map code-as-string args))) ")"))

; -- message templates ------------------------------------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion src/implant/dirac/implant/intercom.cljs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
[goog.functions :as gfns])
(:import goog.net.WebSocket.ErrorEvent))

(defonce required-repl-api-version 4)
(defonce required-repl-api-version 5)

(defonce ^:dynamic *debugger-events-subscribed* false)
(defonce ^:dynamic *repl-connected* false)
Expand Down
57 changes: 57 additions & 0 deletions src/runtime/dirac/runtime/bootstrap.cljs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
(ns dirac.runtime.bootstrap
(:require [clojure.browser.repl :as brepl]))

; boostrap must be called after document finishes loading, because there may be some code coming after the bootstrap call
; which expects goog.require to work synchronously, like @bendlas discovered

(def ^:dynamic *boostrapped?* false)
(def ^:dynamic *boostrap-timeout* 100)
(def ^:dynamic *boostrap-listeners* (array)) ; a native array of plain functions

; -- async bootstrap wrapper ------------------------------------------------------------------------------------------------

(defn notify-listeners! [listeners]
(doseq [listener listeners]
(listener)))

(defn boostrap-if-needed! []
(when-not *boostrapped?*
(brepl/bootstrap)
(set! *boostrapped?* true))
(notify-listeners! *boostrap-listeners*)
(set! *boostrap-listeners* (array)))

(defn call-after-document-finished-loading [f timeout]
; Our strategy:
; If document is already loaded, we simply execute our function.
; If not, we try to schedule another check with timeout 0ms (fast path).
; If still not loaded, we repeatedly schedule future checks with timeout 100ms (until loaded).
; The magic constant *boostrap-timeout* (100ms) should be good enough for our bootstrapping use-case:
; 1. we don't want to starve event queue in case of slow loading, hence non-zero timeouts
; 2. but we also want to minimize the risk of potentially missing first eval request expecting bootstrapped env
; note: our own `evaluate-javascript` has a guard, but there could be some other code doing their own evals
;
; We use polling here because we cannot assume anything about user's code.
; We do not want to hook DOMContentLoaded or onreadystatechange events which could interfere with user's own handlers.
;
(if (= (.-readyState js/document) "loading")
(js/setTimeout #(call-after-document-finished-loading f *boostrap-timeout*) timeout)
(f)))

(defn bootstrap!
"Reusable browser REPL bootstrapping. Patches the essential functions
in goog.base to support re-loading of namespaces after page load.
Note that this function might do its job asynchronously if at the time of calling the document is still loading.
You may provide a callback which will be called immediatelly after bootstrapping happens.
It has no effect if called after bootstrapping has been already done. Only the callback is called immediatelly."
([] (bootstrap! nil))
([callback]
; patching goog methods before document finished loading can unexpectedly break following goog.require code:
; `<script>goog.require("foo.bar");</script><script>foo.bar.some()</script>`
; It doesn't work as expected any more, as described here: https://developers.google.com/closure/library/docs/gettingstarted#wha
(when (some? callback)
(assert (fn? callback) (str "The callback parameter to clojure.browser.repl/bootstrap expected to be a function."
"Got " (type callback) " instead."))
(.push *boostrap-listeners* callback))
(call-after-document-finished-loading boostrap-if-needed! 0)))
17 changes: 11 additions & 6 deletions src/runtime/dirac/runtime/repl.cljs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
(ns dirac.runtime.repl
(:require-macros [dirac.runtime.repl :refer [with-safe-printing]])
(:require [goog.object]
[clojure.browser.repl :as brepl]
[dirac.runtime.prefs :refer [get-prefs pref]]
[dirac.runtime.bootstrap :refer [bootstrap!]]
[clojure.string :as string]
[goog.object :as gobject]
[goog.labs.userAgent.browser :as ua]))
Expand All @@ -20,6 +20,7 @@
(and (ua/isChrome) (ua/isVersionOrHigher 47))) ; Chrome 47+

(def ^:dynamic *installed?* false)
(def ^:dynamic *bootstrapped?* false)

; keep in mind that we want to avoid any state at all
; javascript running this code can be reloaded anytime, same with devtools front-end
Expand Down Expand Up @@ -111,7 +112,7 @@

; -- REPL API ---------------------------------------------------------------------------------------------------------------

(def api-version 4) ; version of REPL API
(def api-version 5) ; version of REPL API

(defn ^:export get-api-version []
api-version)
Expand Down Expand Up @@ -169,17 +170,21 @@
(assert (string? code) "Code passed for evaluation must be a string")
(call-dirac "eval-js" code))

(defn ^:export bootstrapped? []
*bootstrapped?*)

; -- install/uninstall ------------------------------------------------------------------------------------------------------

(defn installed? []
(defn ^:export installed? []
*installed?*)

(defn install! []
(defn ^:export install! []
(when (not (installed?))
(brepl/bootstrap)
(bootstrap! #(set! *bootstrapped?* true))
(set! *installed?* true)
true))

(defn uninstall! []
(defn ^:export uninstall! []
(when (installed?)
(set! *installed?* false)))

4 changes: 2 additions & 2 deletions test/browser/transcripts/expected/suite01-version-checks.txt
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ devtools #5 init-repl!
devtools #5 setDiracPromptStatusContent('Checking for Dirac Runtime presence in your app...')
devtools #5 setDiracPromptStatusStyle('info')
devtools #5 setDiracPromptStatusContent('Dirac REPL API version mismatch detected.
Dirac DevTools requires Dirac Runtime REPL API v4, but your version is v0.
Dirac DevTools requires Dirac Runtime REPL API v5, but your version is v0.
Please <a href="https://github.com/binaryage/dirac/blob/master/docs/upgrading.md">upgrade Dirac Runtime</a> in your app.')
devtools #5 setDiracPromptStatusStyle('error')
automate #5 close-devtools!
Expand Down Expand Up @@ -168,7 +168,7 @@ devtools #6 init-repl!
devtools #6 setDiracPromptStatusContent('Checking for Dirac Runtime presence in your app...')
devtools #6 setDiracPromptStatusStyle('info')
devtools #6 setDiracPromptStatusContent('Dirac REPL API version mismatch detected.
Dirac DevTools requires Dirac Runtime REPL API v4, but your version is v1000.
Dirac DevTools requires Dirac Runtime REPL API v5, but your version is v1000.
Please <a href="https://github.com/binaryage/dirac/blob/master/docs/upgrading.md">upgrade Dirac Runtime</a> in your app.')
devtools #6 setDiracPromptStatusStyle('error')
automate #6 close-devtools!
Expand Down

0 comments on commit e5f42d3

Please sign in to comment.