-
Notifications
You must be signed in to change notification settings - Fork 2
/
Ref.purs
72 lines (65 loc) · 3.47 KB
/
Ref.purs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
module Elmish.Ref
( Ref, ref, deref
) where
import Prelude
import Data.Maybe (fromJust, isJust)
import Foreign.Object as M
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
import Partial.Unsafe (unsafePartial)
import Unsafe.Coerce (unsafeCoerce)
import Elmish.Foreign (class CanPassToJavaScript, class CanReceiveFromJavaScript)
-- | An opaque reference for tunneling through JSX code.
-- |
-- | This type is a wrapper that lets us pass any PureScript values into JSX
-- | code, with the expectation that the JSX code cannot mess with (inspect,
-- | mutate) these values, but can pass them back to the PureScript code in
-- | messages. This type has instances of `CanPassToJavaScript` and
-- | `CanReceiveFromJavaScript`, which allows it to be passed in React props or
-- | view messages.
-- |
-- | One challenge with this type is that we can't just `unsafeCoerce` its
-- | values back and forth, because that would open a very big hole for data
-- | corruption to get in. To have some protection against it, we add a weak
-- | form of verification: internally values of `Ref` are represented by a
-- | JavaScript hash with a sole key looking like "ref:name", whose value is the
-- | target of the ref, and where "name" is the first type argument of this
-- | `Ref`. This way, we have at least _something_ to verify (see the
-- | `CanReceiveFromJavaScript` instance below) that the object passed by the
-- | JSX code is not some random value, but actually originated as a `Ref a` of
-- | the right type.
-- |
-- | Admittedly, this is only weak protection, because the JSX code can still,
-- | if it really wanted to, construct a hash like `{ "ref:name": "abracadabra"}`
-- | and pass it to the PureScript code, which would happily
-- | accept the "abracadabra" value as if it was the right type.
-- |
-- | Here are my arguments for why this weak protection is enough:
-- | 1) The JSX code has to actually _try_ to be destructive. Can't happen by
-- | accident.
-- | 2) It's technically impossible to do any better without putting
-- | significant restrictions on the type `a` (i.e. requiring it to be
-- | `Generic` or to provide type name, etc.), and without losing some
-- | performance.
-- | 3) If such corruption proves to be a problem in the future, we can always
-- | fall back to encoding/decoding `Json`, and pay some performance for it.
-- |
newtype Ref (name :: Symbol) a = Ref (M.Object a)
-- | Creates an instance of `Ref`. See comments on it above.
ref :: ∀ name a. IsSymbol name => a -> Ref name a
ref a = Ref $ M.singleton (refName (SProxy :: SProxy name)) a
-- | Deconstructs an instance of `Ref`. See comments on it above.
deref :: ∀ name a. IsSymbol name => Ref name a -> a
deref (Ref m) =
-- This use of `fromJust` is justified, because the `Ref` constructor is not exported,
-- and the only two places where values of this type are constructed (`ref` above and
-- `CanReceiveFromJavaScript` below) guarantee that this key will be present.
unsafePartial $ fromJust $ M.lookup (refName (SProxy :: SProxy name)) m
refName :: ∀ name. IsSymbol name => SProxy name -> String
refName p = "ref:" <> reflectSymbol p
-- See comments on `Ref` above.
instance readjsRef :: IsSymbol name => CanReceiveFromJavaScript (Ref name a) where
isForeignOfCorrectType _ v = isJust $ M.lookup sname map
where
sname = refName (SProxy :: SProxy name)
map = unsafeCoerce v
instance writejsRef :: IsSymbol name => CanPassToJavaScript (Ref name a)