git.iko.soy/rowtype-yoga/purescript-supabase
on main help sign in
Add support for realtime (#7)
commit3d7a27063bbe2d73daed29436616a5aa3fd16cf9 authorMark Eibes <mark.eibes@gmail.com> dateTue 11 Apr 2023, 10:21 (4 years ago) parentcb37f98
10 files changed+153−16
spago.dhall+4 src/Supabase/AuthHelpers.js+2−1 src/Supabase/AuthHelpers.purs+4−2 src/Supabase/Realtime.js+5 src/Supabase/Realtime.purs+59 src/Supabase/Realtime/ListenType.purs+18 src/Supabase/Realtime/SubscribeStates.purs+25 src/Supabase/Supabase.js+4 src/Supabase/Supabase.purs+27−10 src/Supabase/Types.purs+5−3
spago.dhall+4
@@ -6,6 +6,7 @@
66 , "datetime"
77 , "effect"
88 , "either"
9+ , "exceptions"
910 , "fetch"
1011 , "fetch-core"
1112 , "foldable-traversable"
@@ -21,6 +22,9 @@
2122 , "transformers"
2223 , "tuples"
2324 , "typelevel-prelude"
25+ , "unsafe-coerce"
26+ , "unsafe-reference"
27+ , "untagged-union"
2428 , "web-file"
2529 , "yoga-json"
2630 ]
src/Supabase/AuthHelpers.js+2−1
@@ -1,8 +1,9 @@
1+'use client'
12import { useUser } from "@supabase/auth-helpers-react";
3+import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
24
35export const useUserImpl = useUser;
46
5−import { createBrowserSupabaseClient } from "@supabase/auth-helpers-nextjs";
67
78export const createBrowserClient = () => createBrowserSupabaseClient();
89
src/Supabase/AuthHelpers.purs+4−2
@@ -20,6 +20,7 @@ import Record.Studio (mapRecordKind)
2020import Supabase.Supabase as Supabase
2121import Supabase.Types (Client)
2222import Yoga.JSON as YogaJSON
23+import Yoga.JSON (class WriteForeign)
2324
2425foreign import useUserImpl :: Effect { user :: Nullable Supabase.User }
2526
@@ -43,14 +44,15 @@ type Options =
4344 { db :: Maybe String
4445 }
4546
46−type ClientOptions =
47+type ClientOptions r =
4748 { cookieOptions :: Maybe CookieOptions
4849 , options :: Maybe Options
4950 , supabaseKey :: Maybe String
5051 , supabaseUrl :: Maybe String
52+ | r
5153 }
5254
5355foreign import createBrowserClientWithOptionsInternal :: Foreign -> Effect Client
5456
55−createBrowserClientWithOptions ∷ ClientOptions → Effect Client
57+createBrowserClientWithOptions ∷ forall r. WriteForeign (ClientOptions r) => ClientOptions r → Effect Client
5658createBrowserClientWithOptions = YogaJSON.write >>> createBrowserClientWithOptionsInternal
src/Supabase/Realtime.jsnew file+5
@@ -0,0 +1,5 @@
1+export const sendImpl = (what, channel) => channel.send(what)
2+
3+export const onImpl = (listenType, filter, callback, channel) => channel.on(listenType, filter, callback)
4+
5+export const subscribeImpl = (callback,timeout, channel) => channel.subscribe(callback,timeout)
src/Supabase/Realtime.pursnew file+59
@@ -0,0 +1,59 @@
1+module Supabase.Realtime where
2+
3+import Prelude
4+import Effect.Uncurried (EffectFn1, EffectFn2, EffectFn3, EffectFn4, mkEffectFn1, mkEffectFn2, runEffectFn2, runEffectFn3, runEffectFn4)
5+import Supabase.Types (Channel)
6+import Control.Promise (Promise, toAffE)
7+import Effect.Aff (Aff)
8+import Effect (Effect)
9+import Unsafe.Coerce (unsafeCoerce)
10+import Supabase.Realtime.ListenType (RealtimeListenType)
11+import Supabase.Realtime.SubscribeStates (RealtimeSubscribeState, RealtimeSubscribeState(ChannelError))
12+import Effect.Exception (Error, error)
13+import Untagged.Union (UndefinedOr, uorToMaybe)
14+import Data.Time.Duration (Milliseconds)
15+import Data.Maybe (Maybe, Maybe(Nothing), Maybe(Just))
16+import Supabase.Realtime.SubscribeStates (fromString) as SubscribeState
17+import Control.Monad.Error.Class (throwError)
18+
19+data RealtimeResponse = SendingOK | SendingTimedOut | SendingRateLimited
20+
21+foreign import sendImpl :: forall i. EffectFn2 { | i } Channel (Promise String)
22+
23+send :: forall i. { | i } -> Channel -> Aff RealtimeResponse
24+send input channel = do
25+ res <- runEffectFn2 sendImpl input channel # toAffE
26+ case res of
27+ "ok" -> pure SendingOK
28+ "timed out" -> pure SendingTimedOut
29+ "rate limited" -> pure SendingRateLimited
30+ _ -> throwError $ error $ "Supabase.Realtime.send: invalid response: " <> res
31+
32+foreign import onImpl :: forall f cbi. EffectFn4 RealtimeListenType f (EffectFn1 cbi Unit) Channel Unit
33+
34+on :: forall f cbi. RealtimeListenType -> f -> (cbi -> Effect Unit) -> Channel -> Effect Unit
35+on lt filter callback channel = runEffectFn4 onImpl lt filter (mkEffectFn1 callback) channel
36+
37+foreign import subscribeImpl
38+ :: EffectFn3
39+ (EffectFn2 String (UndefinedOr Error) Unit)
40+ Milliseconds
41+ Channel
42+ Unit
43+
44+subscribe
45+ :: Milliseconds
46+ -> (RealtimeSubscribeState -> Maybe Error -> Effect Unit)
47+ -> Channel
48+ -> Effect Unit
49+subscribe timeout cb channel =
50+ runEffectFn3 subscribeImpl
51+ ( mkEffectFn2 \st err ->
52+ case SubscribeState.fromString st of
53+ Nothing ->
54+ cb ChannelError (Just (error $ "Supabase.Realtime.subscribe: invalid subscribe state: " <> st))
55+ Just ok ->
56+ cb ok (uorToMaybe err)
57+ )
58+ timeout
59+ channel
src/Supabase/Realtime/ListenType.pursnew file+18
@@ -0,0 +1,18 @@
1+module Supabase.Realtime.ListenType where
2+
3+import Prelude
4+import Unsafe.Coerce (unsafeCoerce)
5+import Unsafe.Reference (unsafeRefEq)
6+
7+foreign import data RealtimeListenType :: Type
8+instance Eq RealtimeListenType where
9+ eq = unsafeRefEq
10+
11+broadcast :: RealtimeListenType
12+broadcast = unsafeCoerce "broadcast"
13+
14+presence :: RealtimeListenType
15+presence = unsafeCoerce "presence"
16+
17+postgresChanges :: RealtimeListenType
18+postgresChanges = unsafeCoerce "postgres_changes"
src/Supabase/Realtime/SubscribeStates.pursnew file+25
@@ -0,0 +1,25 @@
1+module Supabase.Realtime.SubscribeStates where
2+
3+import Prelude
4+import Unsafe.Coerce (unsafeCoerce)
5+import Unsafe.Reference (unsafeRefEq)
6+import Data.Maybe (Maybe(Nothing), Maybe, Maybe(Just))
7+
8+data RealtimeSubscribeState = Subscribed | TimedOut | Closed | ChannelError
9+derive instance Eq RealtimeSubscribeState
10+
11+toString :: RealtimeSubscribeState -> String
12+toString = case _ of
13+ Subscribed -> "SUBSCRIBED"
14+ TimedOut -> "TIMED_OUT"
15+ Closed -> "CLOSED"
16+ ChannelError -> "CHANNEL_ERROR"
17+
18+fromString :: String -> Maybe RealtimeSubscribeState
19+fromString = case _ of
20+ "SUBSCRIBED" -> Just Subscribed
21+ "TIMED_OUT" -> Just TimedOut
22+ "CLOSED" -> Just Closed
23+ "CHANNEL_ERROR" -> Just ChannelError
24+ _ -> Nothing
25+
src/Supabase/Supabase.js+4
@@ -8,6 +8,8 @@ export const getSessionImpl = (supabase) => () => supabase.auth.getSession();
88export const onAuthStateChangeImpl = (supabase) => (handler) => () =>
99 supabase.auth.onAuthStateChange((_event, session) => handler(session));
1010
11+export const getUserImpl = supabase => () => supabase.auth.getUser();
12+
1113export const signOutImpl = (supabase) => () => supabase.auth.signOut();
1214export const fromImpl = (client) => (table) => client.from(table);
1315export const selectQueryImpl = (queryBuilder) => (projection) =>
@@ -45,3 +47,5 @@ export const invokeImpl = (client) => (functionName, body, headers) => () =>
4547 body,
4648 headers,
4749 });
50+
51+export const channel = (channelName) => (client) => () => client.channel(channelName);
src/Supabase/Supabase.purs+27−10
@@ -19,6 +19,7 @@ module Supabase.Supabase
1919 , class Select
2020 , delete
2121 , equals
22+ , channel
2223 , from
2324 , getSession
2425 , invoke
@@ -26,6 +27,7 @@ module Supabase.Supabase
2627 , onAuthStateChange
2728 , range
2829 , select
30+ , getUser
2931 , signInWithOtp
3032 , signInWithOtpOptions
3133 , signOut
@@ -37,26 +39,28 @@ module Supabase.Supabase
3739import Prelude
3840
3941import Control.Promise (Promise)
40−import Control.Promise as Promise
42+import Data.Either (Either(..))
4143import Data.Function.Uncurried (Fn2, Fn3, runFn2, runFn3)
42−import Data.Maybe (Maybe)
44+import Data.Maybe (Maybe(..))
4345import Data.Nullable (Nullable)
44−import Data.Nullable as Nullable
4546import Data.Tuple (Tuple(..))
4647import Effect (Effect)
4748import Effect.Aff (Aff, Error)
49+import Effect.Exception (error)
4850import Effect.Uncurried (EffectFn1, EffectFn3, mkEffectFn1, runEffectFn3)
49−import Fetch as Fetch
50−import Fetch.Core.Response as YogaJson.Core
51−import Fetch.Internal.Response as FetchInternalResponse
5251import Foreign (Foreign)
5352import Prim.Row (class Union)
54−import Supabase.Types (Client)
55−import Supabase.Util as Util
53+import Supabase.Types (Channel, ChannelName, Client)
5654import Type.Function (type ($))
5755import Type.Row (type (+))
58−import Yoga.JSON (class ReadForeign, class WriteForeign, write) as YogaJson
5956import Yoga.JSON (class ReadForeign, class WriteForeign, writeImpl)
57+import Fetch as Fetch
58+import Fetch.Internal.Response as FetchInternalResponse
59+import Data.Nullable as Nullable
60+import Control.Promise as Promise
61+import Supabase.Util as Util
62+import Yoga.JSON (class ReadForeign, class WriteForeign, write) as YogaJson
63+import Fetch.Core.Response as YogaJson.Core
6064
6165foreign import data QueryBuilder :: Type
6266foreign import data FilterBuilder :: Type
@@ -169,7 +173,7 @@ foreign import onAuthStateChangeImpl :: Client -> (EffectFn1 (Nullable Session)
169173onAuthStateChange ∷ Client → (Maybe Session → Effect Unit) → Effect { data :: { id :: String, unsubscribe :: Effect Unit } }
170174onAuthStateChange client handler = onAuthStateChangeImpl client $ mkEffectFn1 (Nullable.toMaybe >>> handler)
171175
172−type User = { id :: String, email :: String }
176+type User = { id :: String, email :: String, user_metadata :: Foreign }
173177type Session = { user :: User }
174178type SessionData = { session :: Maybe Session }
175179
@@ -200,3 +204,16 @@ invoke client fn body headers = runFn3 (invokeImpl client) fn body headers # Pro
200204 where
201205 convertError { message, context } = { message, context: FetchInternalResponse.convert context }
202206 convert { "data": d, error } = { "data": Nullable.toMaybe d, error: Nullable.toMaybe error <#> convertError }
207+
208+foreign import channel :: ChannelName -> Client -> Effect Channel
209+
210+foreign import getUserImpl :: Client -> Effect (Promise { data :: Nullable User, error :: Nullable Error })
211+
212+getUser :: Client -> Aff (Either Error User)
213+getUser client = do
214+ res <- getUserImpl client # Promise.toAffE
215+ case Nullable.toMaybe res.data, Nullable.toMaybe res.error of
216+ Just user, _ -> pure $ Right user
217+ _, Just error -> pure $ Left error
218+ _, _ -> pure $ Left (error "Unexpected response error")
219+
src/Supabase/Types.purs+5−3
@@ -1,7 +1,9 @@
1−module Supabase.Types
2− ( Client
3− ) where
1+module Supabase.Types where
42
53import Prelude
64
75foreign import data Client :: Type
6+
7+foreign import data Channel :: Type
8+
9+newtype ChannelName = ChannelName String