Skip to content

Commit 1316f9f

Browse files
committed
SuspenseStore refactor to allow caching of any HasBackend instance members
1 parent 4e83f47 commit 1316f9f

File tree

3 files changed

+180
-43
lines changed

3 files changed

+180
-43
lines changed

src/React/Basic/Hooks/Suspense.purs

+4
Original file line numberDiff line numberDiff line change
@@ -41,11 +41,15 @@ suspend (Suspended e) = React.do
4141
newtype Suspended a
4242
= Suspended (Effect (SuspenseResult a))
4343

44+
derive instance functorSuspended :: Functor Suspended
45+
4446
data SuspenseResult a
4547
= InProgress (Fiber a)
4648
| Failed Error
4749
| Complete a
4850

51+
derive instance functorSuspenseResult :: Functor SuspenseResult
52+
4953
suspense :: { fallback :: JSX, children :: Array JSX } -> JSX
5054
suspense = element suspense_
5155

+94-43
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,55 @@
11
module React.Basic.Hooks.Suspense.Store
2-
( mkSuspenseStore
3-
, SuspenseStore
2+
( SuspenseStore
3+
, mkSuspenseStore
44
, get
55
, get'
6+
, class HasBackend
7+
, fromKey
8+
, backend
69
) where
710

811
import Prelude
12+
913
import Control.Alt ((<|>))
10-
import Data.DateTime.Instant (Instant, unInstant)
14+
import Data.DateTime.Instant (unInstant)
1115
import Data.Either (Either(..))
16+
import Data.Function (on)
1217
import Data.Int (ceil)
13-
import Data.Map (Map)
1418
import Data.Map as Map
1519
import Data.Maybe (Maybe(..))
1620
import Data.Newtype (un)
21+
import Data.Ord (greaterThan)
22+
import Data.String (joinWith)
23+
import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol)
1724
import Data.Time.Duration (Milliseconds(..))
25+
import Data.Tuple (fst, snd)
1826
import Effect (Effect)
1927
import Effect.Aff (Aff, attempt, launchAff, throwError)
2028
import Effect.Class (liftEffect)
2129
import Effect.Console (warn)
2230
import Effect.Exception (try)
2331
import Effect.Now (now)
24-
import Effect.Ref (Ref)
2532
import Effect.Ref as Ref
26-
import React.Basic.Hooks (type (/\), (/\))
33+
import React.Basic.Hooks ((/\))
2734
import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult(..))
35+
import Unsafe.Coerce (unsafeCoerce)
2836
import Web.HTML (window)
2937
import Web.HTML.Window (requestIdleCallback)
3038

31-
-- | Simple key-based cache.
39+
get :: forall k v s. HasBackend k v s => SuspenseStore -> k -> Suspended v
40+
get s k = _get s Nothing k
41+
42+
get' :: forall k v s. HasBackend k v s => SuspenseStore -> Milliseconds -> k -> Suspended v
43+
get' s d k = _get s (Just d) k
44+
45+
class IsSymbol s <= HasBackend k v (s :: Symbol) | k -> s, k -> v where
46+
fromKey :: k -> String
47+
backend :: k -> Aff v
48+
3249
mkSuspenseStore ::
33-
forall k v.
34-
Ord k =>
3550
Maybe Milliseconds ->
36-
(k -> Aff v) ->
37-
Effect (SuspenseStore k v)
38-
mkSuspenseStore defaultMaxAge backend = do
51+
Effect SuspenseStore
52+
mkSuspenseStore defaultMaxAge = do
3953
ref <- Ref.new mempty
4054
let
4155
isExpired maxAge now' (_ /\ d) = unInstant now' < unInstant d <> maxAge
@@ -68,59 +82,96 @@ mkSuspenseStore defaultMaxAge backend = do
6882
else
6983
pure (Just r)
7084

71-
getCacheOrBackend itemMaxAge k = do
85+
insertIfNewer =
86+
Map.insertWith \r' r ->
87+
let
88+
gt = greaterThan `on` snd
89+
in
90+
if r `gt` r' then r else r'
91+
92+
getCacheOrBackend :: Maybe Milliseconds -> Storable -> Effect (SuspenseResult Opaque)
93+
getCacheOrBackend itemMaxAge storable = do
94+
let
95+
k = toKey storable
7296
c <- tryFromCache itemMaxAge k
7397
case c of
7498
Just v -> pure v
7599
Nothing -> do
76100
fiber <-
77101
launchAff do
78-
r <- attempt do backend k
102+
r <- attempt do toAffUnsafe storable
79103
liftEffect do
80104
let
81105
v = case r of
82106
Left e -> Failed e
83107
Right v' -> Complete v'
84108
d <- now
85-
_ <-
86-
ref
87-
# Ref.modify
88-
( k
89-
# Map.alter case _ of
90-
Nothing -> Just (v /\ d)
91-
Just r'@(v' /\ d') ->
92-
if d > d' then
93-
Just (v /\ d)
94-
else
95-
Just r'
96-
)
109+
_ <- ref # Ref.modify (insertIfNewer k (v /\ d))
97110
case r of
98111
Left e -> throwError e
99112
Right v' -> pure v'
100-
let
101-
v = InProgress fiber
102-
d <- now
103-
_ <- ref # Ref.modify (Map.insert k (v /\ d))
104-
pure v
113+
syncV <- map fst <$> Map.lookup k <$> Ref.read ref
114+
case syncV of
115+
-- `Just v` means the backend `Aff` ran synchronously so
116+
-- we just return that result
117+
Just v -> pure v
118+
Nothing -> do
119+
let
120+
v = InProgress fiber
121+
d <- now
122+
_ <- ref # Ref.modify (insertIfNewer k (v /\ d))
123+
pure v
105124
do
106125
r <- try pruneCache
107126
case r of
108127
Left _ -> warn "Failed to initialize the suspense store cleanup task. Ensure you're using it in a browser with `requestIdleCallback` support."
109128
Right _ -> pure unit
110-
pure
111-
$ SuspenseStore
112-
{ cache: ref
113-
, get: map Suspended <<< getCacheOrBackend
114-
}
129+
pure $ SuspenseStore { get: getCacheOrBackend }
115130

116-
newtype SuspenseStore k v
131+
newtype SuspenseStore
117132
= SuspenseStore
118-
{ cache :: Ref (Map k (SuspenseResult v /\ Instant))
119-
, get :: Maybe Milliseconds -> k -> Suspended v
133+
{ get :: Maybe Milliseconds -> Storable -> Effect (SuspenseResult Opaque)
120134
}
121135

122-
get :: forall k v. SuspenseStore k v -> k -> Suspended v
123-
get (SuspenseStore s) = s.get Nothing
136+
_get :: forall k v s. HasBackend k v s => SuspenseStore -> Maybe Milliseconds -> k -> Suspended v
137+
_get (SuspenseStore s) d k =
138+
Suspended do
139+
let
140+
storable = mkStorable k
141+
r <- s.get d storable
142+
pure (map (fromOpaque k) r)
143+
144+
-- An opaque "cacheable". `Storable` packages up a `HasBackend` instance
145+
-- so the cache can use its `k -> String` and `k -> Aff v` functions
146+
-- without knowing about the internal types stored within the cache.
147+
data Storable
148+
= Storable
149+
(forall x. (forall k v s. HasBackend k v s => k -> x) -> x)
150+
151+
mkStorable :: forall k v s. HasBackend k v s => k -> Storable
152+
mkStorable k = Storable \f -> f k
153+
154+
class ToKey k where
155+
toKey :: k -> String
156+
157+
instance toKeyStorable :: ToKey Storable where
158+
toKey (Storable impl) = impl \k -> joinWith "" [ typeKey k, "[ ", fromKey k, " ]" ]
159+
where
160+
typeKey :: forall k v s. HasBackend k v s => k -> String
161+
typeKey _ = reflectSymbol (SProxy :: _ s)
162+
163+
class ToAffUnsafe k where
164+
toAffUnsafe :: k -> Aff Opaque
165+
166+
instance toAffUnsafeStorable :: ToAffUnsafe Storable where
167+
toAffUnsafe (Storable impl) = impl \k -> map (toOpaque k) (backend k)
168+
169+
data Opaque
170+
171+
class HasOpaque k v | k -> v where
172+
toOpaque :: k -> v -> Opaque
173+
fromOpaque :: k -> Opaque -> v
124174

125-
get' :: forall k v. SuspenseStore k v -> Milliseconds -> k -> Suspended v
126-
get' (SuspenseStore s) d = s.get (Just d)
175+
instance hasOpaque :: HasBackend k v s => HasOpaque k v where
176+
toOpaque _ = unsafeCoerce
177+
fromOpaque _ = unsafeCoerce

test/Test/Main.purs

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
module Test.Main where
2+
3+
import Prelude
4+
import Data.Maybe (Maybe(..))
5+
import Effect (Effect)
6+
import Effect.Aff (Milliseconds(..), delay, launchAff_)
7+
import Effect.Class (liftEffect)
8+
import Effect.Console (log)
9+
import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult)
10+
import React.Basic.Hooks.Suspense.Store (class HasBackend, fromKey, get, mkSuspenseStore)
11+
import Unsafe.Coerce (unsafeCoerce)
12+
13+
main :: Effect Unit
14+
main = do
15+
store <- mkSuspenseStore (Just $ Milliseconds 200.0)
16+
let
17+
runGet :: forall k v s. HasBackend k v s => k -> Effect (SuspenseResult v)
18+
runGet k = case get store k of Suspended r' -> r'
19+
20+
c1 = Key "1" :: Key Cat
21+
22+
c2 = Key "2" :: Key Cat
23+
24+
d1 = Key "1" :: Key Dog
25+
26+
d2 = Key "2" :: Key Dog
27+
28+
go = do
29+
c1' <- runGet c1
30+
d1' <- runGet d1
31+
c2' <- runGet c2
32+
d2' <- runGet d2
33+
c1'' <- runGet c1
34+
d1'' <- runGet d1
35+
c2'' <- runGet c2
36+
d2'' <- runGet d2
37+
l c1'
38+
l c1''
39+
l c2'
40+
l c2''
41+
l d1'
42+
l d1''
43+
l d2'
44+
l d2''
45+
go
46+
launchAff_ do
47+
delay (Milliseconds 100.0)
48+
liftEffect go
49+
delay (Milliseconds 200.0)
50+
liftEffect go
51+
liftEffect go
52+
where
53+
l :: forall v. v -> Effect Unit
54+
l v = do
55+
log (unsafeCoerce v)
56+
57+
newtype Key v
58+
= Key String
59+
60+
derive instance eqKey :: Eq (Key v)
61+
62+
data Cat
63+
= Cat { name :: String }
64+
65+
derive instance eqCat :: Eq Cat
66+
67+
data Dog
68+
= Dog { name :: String }
69+
70+
derive instance eqDog :: Eq Dog
71+
72+
instance backendCat :: HasBackend (Key Cat) Cat "Cat" where
73+
fromKey (Key key) = key
74+
backend key = do
75+
delay $ Milliseconds 0.0
76+
pure $ Cat { name: fromKey key }
77+
78+
instance backendDog :: HasBackend (Key Dog) Dog "Dog" where
79+
fromKey (Key key) = key
80+
backend key = do
81+
delay $ Milliseconds 0.0
82+
pure $ Dog { name: fromKey key }

0 commit comments

Comments
 (0)