Skip to content

Instantly share code, notes, and snippets.

@lilactown
Last active November 14, 2019 11:58
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lilactown/3e59124bfb4fd8a1b5bf70667f1bad63 to your computer and use it in GitHub Desktop.
Save lilactown/3e59124bfb4fd8a1b5bf70667f1bad63 to your computer and use it in GitHub Desktop.
(ns ueaq.core
(:require
[goog.object :as gobj]
["react" :as r]
["react-dom/server" :as rds]))
(defn unwrap
[^js p]
(.-__unwrap p))
(defn shallow-clj->js
[m]
(loop [entries (seq m)
o #js {}]
(if (nil? entries)
o
(let [entry (first entries)
k (key entry)
v (val entry)]
(recur
(next entries)
(doto o
(gobj/set (name k) v)))))))
(declare ueaq)
;; -- traps
(defn getter
[o prop]
(this-as handler
(let [opts (.-opts ^js handler)
context (.-context ^js handler)
{:keys [recursive? prop->key]} opts
v (get o (prop->key prop)
(gobj/get context prop))]
(if (and recursive? (associative? v))
(ueaq v opts)
v))))
(defn has
[o prop]
(this-as handler
(let [{:keys [prop->key]} (.-opts ^js handler)]
(contains? o (prop->key prop)))))
(defn own-keys
[o]
(to-array (map name (keys o))))
(defn enumerate
[o]
(map name (keys o)))
(defn get-own-property-descriptor
[o prop]
(this-as handler
(let [{:keys [prop->key]} (.-opts ^js handler)
k (prop->key prop)]
(if (contains? o k)
#js {:enumerable true :configurable true
:writable false :value (get o k)}
js/undefined))))
(defn get-prototype-of
[_]
(this-as handler
(.-context ^js handler)))
(defn setter
[_ k v]
(this-as handler
(let [context (.-context ^js handler)]
(gobj/set context k v))))
(defn ^js ueaq
([o] (ueaq o {}))
([o {:keys [recursive? prop->key mutable?] :as opts
:or {recursive? false
prop->key keyword}}]
(let [;; this is an object to hold implementations of various protocols for
;; CLJS usage
context (specify! #js {}
Object
(toString [this]
(pr-str* this))
IPrintWithWriter
(-pr-writer [_ writer _]
;; prn is not a fast path
(-write writer
(if recursive?
(pr-str (clj->js o))
(pr-str (shallow-clj->js o))))))
handler #js {:opts (assoc opts
:prop->key prop->key)
:context context
:get getter
:has has
:ownKeys own-keys
:enumerate enumerate
:getOwnPropertyDescriptor get-own-property-descriptor
:getPrototypeOf get-prototype-of
:set setter}]
(js/Proxy. o handler))))
(comment
(do (def m {:a 1 :b {:c 2}})
(def p (ueaq m {:recursive? true})))
(js/Object.isFrozen (js/Object.freeze p))
(unwrap p)
(js/console.log p)
(str p)
(.-constructor p)
(.-constructor #js {})
(.-a p)
(gobj/get p "a")
(gobj/getValueByKeys p #js ["b" "c"])
(.-b p)
(.-c (.-b p))
(.-does-not-exist p)
(set! (.-x p) 3)
(.-x p)
(.-x m)
)
;; -- Perf
(comment
(defn permutations-str
[els]
(when-let [[f & r] (seq els)]
(let [perms (permutations-str r)]
(cons (str f) (concat perms (map (fn [r] (apply str f r)) perms))))))
(def map-entry
(comp (map keyword)
(map-indexed #(vector %2 %1))))
(do
(do (println "\n-- small")
(simple-benchmark
[m {:a 1 :b 2}]
(.-b (ueaq m))
5000)
(simple-benchmark
[m {:a 1 :b 2}]
(.-b (clj->js m))
5000)
(simple-benchmark
[m {:a 1 :b 2}]
(.-b (shallow-clj->js m))
5000))
(do (println "-- small-ish")
(simple-benchmark
[small-map (into {}
map-entry
"abcdefghijklmnopqrstuvwxyz")]
(.-b (ueaq small-map))
5000)
(simple-benchmark
[small-map (into {}
map-entry
"abcdefghijklmnopqrstuvwxyz")]
(.-b (clj->js small-map))
5000)
(simple-benchmark
[small-map (into {}
map-entry
"abcdefghijklmnopqrstuvwxyz")]
(.-b (shallow-clj->js small-map))
5000))
(do (println "-- bigger")
(simple-benchmark
[big-map (into {}
map-entry
(take 1000 (permutations-str "abcdefgh")))]
(.-b (ueaq big-map))
5000)
(simple-benchmark
[big-map (into {}
map-entry
(take 1000 (permutations-str "abcdefgh")))]
(.-b (clj->js big-map))
5000)
(simple-benchmark
[big-map (into {}
map-entry
(take 1000 (permutations-str "abcdefgh")))]
(.-b (shallow-clj->js big-map))
5000))
(do (println "-- recursive")
(simple-benchmark
[nested-map {:a {:b {:c {:d {:e {:f {:g 1}}}}}}}]
(gobj/getValueByKeys
(ueaq nested-map {:recursive? true})
"a" "b" "c" "d" "e" "f" "g")
5000)
(simple-benchmark
[nested-map {:a {:b {:c {:d {:e {:f {:g 1}}}}}}}]
(gobj/getValueByKeys
(clj->js nested-map)
"a" "b" "c" "d" "e" "f" "g")
5000))
(println "-- end"))
)
(when (exists? js/Symbol)
(extend-protocol IPrintWithWriter
js/Symbol
(-pr-writer [sym writer _]
(-write writer (str "\"" (.toString sym) "\"")))))
(comment
(def props (ueaq {:style #js {:color "green"}}))
(js/console.log props)
(js/Object.prototype.hasOwnProperty.call props "style")
(gobj/equals (ueaq {:a 1}) #js {:a 1})
(gobj/clone (ueaq {:a 1}))
(gobj/getKeys (ueaq {:a 1}))
(js/Object.keys (ueaq {:a 1}))
(js/Reflect.ownKeys (ueaq {:a 1}))
(js/Object.getOwnPropertyNames (ueaq {:a 1}))
(js/Object.getOwnPropertySymbols (ueaq {:a 1}))
(filter #{"cljs$core$ILookup$"} (gobj/getKeys (ueaq {:a 1})))
(filter #{"a"} (gobj/getKeys #js {:a 1}))
(def div (r/createElement "div" props "hi"))
(def div2 (r/createElement "div" #js {:style #js {:color "green"}} "hi"))
(rds/renderToString div)
(rds/renderToString div2)
)
@jimmyhmiller
Copy link

Great work!

I grabbed my old project where I had been doing something similar to if the bug I had seen still existed and I am able to recreate the same issue I had before and sadly I was.

The problem happens when you pass the props in devmode. I don't think it does the same thing with renderToString, so you didn't have the issue. But you can actually recreate the issue without having to load react in a browser. If you run this:

(js/Reflect.ownKeys (ueaq (js/Object.freeze {:a 1})))

You will get the following error:

TypeError: 'ownKeys' on proxy: trap result did not include 'meta'
   at Object.ownKeys

So it seems to be saying that ownkeys needs to return meta because that is one of the properties on our target. So we could try changing own-keys to this:

(defn own-keys
  [o]
  (.concat (to-array (map name (keys o)))
           (js/Reflect.ownKeys o)))

But that just gives us a new error.

TypeError: 'ownKeys' on proxy: trap returned extra keys but proxy                                                   
   target is non-extensible at Object.ownKeys

Sadly this is because of the invariants enforced by proxy. In this case:

If the target object is not extensible, then the result List must contain all the keys of the own properties of the target object and no other values.

So, if our object is frozen the keys we return must match the targets keys exactly. Which means our target has to end up being something like (shallow-clj->js o). Which really sucks.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment