|
1 | 1 | module React.Basic.Hooks.Suspense.Store
|
2 |
| - ( mkSuspenseStore |
3 |
| - , SuspenseStore |
| 2 | + ( SuspenseStore |
| 3 | + , mkSuspenseStore |
4 | 4 | , get
|
5 | 5 | , get'
|
| 6 | + , class HasBackend |
| 7 | + , fromKey |
| 8 | + , backend |
6 | 9 | ) where
|
7 | 10 |
|
8 | 11 | import Prelude
|
9 | 12 | import Control.Alt ((<|>))
|
10 |
| -import Data.DateTime.Instant (Instant, unInstant) |
| 13 | +import Data.DateTime.Instant (unInstant) |
11 | 14 | import Data.Either (Either(..))
|
| 15 | +import Data.Function (on) |
12 | 16 | import Data.Int (ceil)
|
13 |
| -import Data.Map (Map) |
14 | 17 | import Data.Map as Map
|
15 | 18 | import Data.Maybe (Maybe(..))
|
16 | 19 | import Data.Newtype (un)
|
| 20 | +import Data.Ord (greaterThan) |
| 21 | +import Data.String (joinWith) |
| 22 | +import Data.Symbol (class IsSymbol, SProxy(..), reflectSymbol) |
17 | 23 | import Data.Time.Duration (Milliseconds(..))
|
| 24 | +import Data.Tuple (fst, snd) |
18 | 25 | import Effect (Effect)
|
19 | 26 | import Effect.Aff (Aff, attempt, launchAff, throwError)
|
20 | 27 | import Effect.Class (liftEffect)
|
21 | 28 | import Effect.Console (warn)
|
22 | 29 | import Effect.Exception (try)
|
23 | 30 | import Effect.Now (now)
|
24 |
| -import Effect.Ref (Ref) |
25 | 31 | import Effect.Ref as Ref
|
26 |
| -import React.Basic.Hooks (type (/\), (/\)) |
| 32 | +import React.Basic.Hooks ((/\)) |
27 | 33 | import React.Basic.Hooks.Suspense (Suspended(..), SuspenseResult(..))
|
| 34 | +import Unsafe.Coerce (unsafeCoerce) |
28 | 35 | import Web.HTML (window)
|
29 | 36 | import Web.HTML.Window (requestIdleCallback)
|
30 | 37 |
|
31 |
| --- | Simple key-based cache. |
| 38 | +get :: forall k v s. HasBackend k v s => SuspenseStore -> k -> Suspended v |
| 39 | +get s k = _get s Nothing k |
| 40 | + |
| 41 | +get' :: forall k v s. HasBackend k v s => SuspenseStore -> Milliseconds -> k -> Suspended v |
| 42 | +get' s d k = _get s (Just d) k |
| 43 | + |
| 44 | +class |
| 45 | + IsSymbol s <= HasBackend k v (s :: Symbol) | k -> v s where |
| 46 | + fromKey :: k -> String |
| 47 | + backend :: k -> Aff v |
| 48 | + |
32 | 49 | mkSuspenseStore ::
|
33 |
| - forall k v. |
34 |
| - Ord k => |
35 | 50 | Maybe Milliseconds ->
|
36 |
| - (k -> Aff v) -> |
37 |
| - Effect (SuspenseStore k v) |
38 |
| -mkSuspenseStore defaultMaxAge backend = do |
| 51 | + Effect SuspenseStore |
| 52 | +mkSuspenseStore defaultMaxAge = do |
39 | 53 | ref <- Ref.new mempty
|
40 | 54 | let
|
41 | 55 | isExpired maxAge now' (_ /\ d) = unInstant now' < unInstant d <> maxAge
|
@@ -68,59 +82,92 @@ mkSuspenseStore defaultMaxAge backend = do
|
68 | 82 | else
|
69 | 83 | pure (Just r)
|
70 | 84 |
|
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 -> StoreKey -> Effect (SuspenseResult Opaque) |
| 93 | + getCacheOrBackend itemMaxAge storable = do |
| 94 | + let |
| 95 | + k = toKey storable |
72 | 96 | c <- tryFromCache itemMaxAge k
|
73 | 97 | case c of
|
74 | 98 | Just v -> pure v
|
75 | 99 | Nothing -> do
|
76 | 100 | fiber <-
|
77 | 101 | launchAff do
|
78 |
| - r <- attempt do backend k |
| 102 | + r <- attempt do toAff storable |
79 | 103 | liftEffect do
|
80 | 104 | let
|
81 | 105 | v = case r of
|
82 | 106 | Left e -> Failed e
|
83 | 107 | Right v' -> Complete v'
|
84 | 108 | 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)) |
97 | 110 | case r of
|
98 | 111 | Left e -> throwError e
|
99 | 112 | 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 |
105 | 124 | do
|
106 | 125 | r <- try pruneCache
|
107 | 126 | case r of
|
108 | 127 | Left _ -> warn "Failed to initialize the suspense store cleanup task. Ensure you're using it in a browser with `requestIdleCallback` support."
|
109 | 128 | Right _ -> pure unit
|
110 |
| - pure |
111 |
| - $ SuspenseStore |
112 |
| - { cache: ref |
113 |
| - , get: map Suspended <<< getCacheOrBackend |
114 |
| - } |
| 129 | + pure $ SuspenseStore { get: getCacheOrBackend } |
115 | 130 |
|
116 |
| -newtype SuspenseStore k v |
| 131 | +newtype SuspenseStore |
117 | 132 | = SuspenseStore
|
118 |
| - { cache :: Ref (Map k (SuspenseResult v /\ Instant)) |
119 |
| - , get :: Maybe Milliseconds -> k -> Suspended v |
| 133 | + { get :: Maybe Milliseconds -> StoreKey -> Effect (SuspenseResult Opaque) |
120 | 134 | }
|
121 | 135 |
|
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". `StoreKey` 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 StoreKey |
| 148 | + = StoreKey |
| 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 -> StoreKey |
| 152 | +mkStorable k = StoreKey \f -> f k |
| 153 | + |
| 154 | +class Storable k where |
| 155 | + toKey :: k -> String |
| 156 | + toAff :: k -> Aff Opaque |
| 157 | + |
| 158 | +instance storableStoreKey :: Storable StoreKey where |
| 159 | + toKey (StoreKey impl) = impl \k -> joinWith "" [ typeKey k, "[ ", fromKey k, " ]" ] |
| 160 | + where |
| 161 | + typeKey :: forall k v s. HasBackend k v s => k -> String |
| 162 | + typeKey _ = reflectSymbol (SProxy :: _ s) |
| 163 | + toAff (StoreKey impl) = impl \k -> map (toOpaque k) (backend k) |
| 164 | + |
| 165 | +data Opaque |
| 166 | + |
| 167 | +class HasOpaque k v | k -> v where |
| 168 | + toOpaque :: k -> v -> Opaque |
| 169 | + fromOpaque :: k -> Opaque -> v |
124 | 170 |
|
125 |
| -get' :: forall k v. SuspenseStore k v -> Milliseconds -> k -> Suspended v |
126 |
| -get' (SuspenseStore s) d = s.get (Just d) |
| 171 | +instance hasOpaque :: HasBackend k v s => HasOpaque k v where |
| 172 | + toOpaque _ = unsafeCoerce |
| 173 | + fromOpaque _ = unsafeCoerce |
0 commit comments