Skip to content

Commit 8be2cac

Browse files
committed
Initial commit
0 parents  commit 8be2cac

File tree

7 files changed

+508
-0
lines changed

7 files changed

+508
-0
lines changed

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# elm-package generated files
2+
elm-stuff/
3+
# elm-repl generated files
4+
repl-temp-*

LICENSE

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
Copyright (c) 2017, Lukáš Mladý
2+
3+
All rights reserved.
4+
5+
Redistribution and use in source and binary forms, with or without
6+
modification, are permitted provided that the following conditions are met:
7+
8+
* Redistributions of source code must retain the above copyright
9+
notice, this list of conditions and the following disclaimer.
10+
11+
* Redistributions in binary form must reproduce the above
12+
copyright notice, this list of conditions and the following
13+
disclaimer in the documentation and/or other materials provided
14+
with the distribution.
15+
16+
* Neither the name of Lukáš Mladý nor the names of other
17+
contributors may be used to endorse or promote products derived
18+
from this software without specific prior written permission.
19+
20+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
21+
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
22+
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
23+
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
24+
OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
25+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
26+
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
27+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
28+
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
29+
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
30+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

README.md

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
# BroadcastChannel
2+
3+
Communicate across browsing contexts (windows, tabs, frames, iframes, or workers) with the same origin.
4+
5+
See [Can I Use](http://caniuse.com/#feat=broadcastchannel) for browser support.
6+
7+
## Usage
8+
9+
`BroadcastChannel` exposes two function:
10+
11+
- `listen` for creating subscriptions
12+
- `send` for creating commands
13+
14+
### Broadcasting a message
15+
16+
Use `BroadcastChannel.send "test_channel" "my message!"` to create a send command.
17+
18+
### Subscribing to a channel
19+
20+
Use `BroadcastChannel.listen "test_channel" NewMessage` to create a channel subscription.
21+
22+
## Example
23+
24+
```elm
25+
import Html exposing (..)
26+
import Html.Attributes exposing (..)
27+
import Html.Events exposing (..)
28+
import BroadcastChannel
29+
30+
31+
main =
32+
Html.program
33+
{ init = init
34+
, view = view
35+
, update = update
36+
, subscriptions = subscriptions
37+
}
38+
39+
40+
41+
-- MODEL
42+
43+
44+
type alias Model =
45+
{ input : String
46+
, messages : List String
47+
}
48+
49+
50+
init : ( Model, Cmd Msg )
51+
init =
52+
( Model "" [], Cmd.none )
53+
54+
55+
56+
-- UPDATE
57+
58+
59+
type Msg
60+
= Input String
61+
| Send
62+
| NewMessage String
63+
64+
65+
update : Msg -> Model -> ( Model, Cmd Msg )
66+
update msg { input, messages } =
67+
case msg of
68+
Input newInput ->
69+
( Model newInput messages, Cmd.none )
70+
71+
Send ->
72+
( Model "" messages, BroadcastChannel.send "test_channel" input )
73+
74+
NewMessage str ->
75+
( Model input (str :: messages), Cmd.none )
76+
77+
78+
79+
-- SUBSCRIPTIONS
80+
81+
82+
subscriptions : Model -> Sub Msg
83+
subscriptions model =
84+
BroadcastChannel.listen "test_channel" NewMessage
85+
86+
87+
88+
-- VIEW
89+
90+
91+
view : Model -> Html Msg
92+
view model =
93+
div []
94+
[ h2 [] [ text "Broadcast a message to other browsing contexts:" ]
95+
, input [ onInput Input, value model.input ] [ text "-" ]
96+
, button [ onClick Send ] [ text "Send" ]
97+
, ul [] (List.map (\item -> li [] [ text item ]) model.messages)
98+
]
99+
```

elm-package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"version": "1.0.0",
3+
"summary": "Communicate across browsing contexts with the same origin in Elm",
4+
"repository": "https://github.com/lukasmlady/elm-broadcast-channel.git",
5+
"license": "MIT",
6+
"source-directories": [
7+
"src"
8+
],
9+
"exposed-modules": [
10+
"BroadcastChannel",
11+
"BroadcastChannel.LowLevel"
12+
],
13+
"native-modules": true,
14+
"dependencies": {
15+
"elm-lang/core": "5.1.1 <= v < 6.0.0"
16+
},
17+
"elm-version": "0.18.0 <= v < 0.19.0"
18+
}

src/BroadcastChannel.elm

Lines changed: 241 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
effect module BroadcastChannel
2+
where { command = MyCmd, subscription = MySub }
3+
exposing
4+
( send
5+
, listen
6+
)
7+
8+
{-| BroadcastChannel makes it possible to talk to other browsing contexts with
9+
the same origin.
10+
11+
Browsing contexts are windows, tabs, frames, iframes and workers.
12+
13+
The API here attempts to cover the typical usage scenarios.
14+
15+
**Note:** This package is heavily inspired by `elm-lang/websocket`.
16+
Most of its code is reused here.
17+
18+
# BroadcastChannel
19+
20+
@docs listen, send
21+
22+
-}
23+
24+
import Dict
25+
import Task exposing (Task)
26+
import BroadcastChannel.LowLevel as BC
27+
28+
29+
-- COMMANDS
30+
31+
32+
type MyCmd msg
33+
= Send String String
34+
35+
36+
{-| Send a message to a particular channel name. You might say something like this:
37+
38+
send "user" "logout"
39+
40+
-}
41+
send : String -> String -> Cmd msg
42+
send name message =
43+
command (Send name message)
44+
45+
46+
cmdMap : (a -> b) -> MyCmd a -> MyCmd b
47+
cmdMap _ (Send url msg) =
48+
Send url msg
49+
50+
51+
52+
-- SUBSCRIPTIONS
53+
54+
55+
type MySub msg
56+
= Listen String (String -> msg)
57+
58+
59+
{-| Subscribe to any incoming messages on a broadcast channel. You might say something
60+
like this:
61+
62+
type Msg = UserLogout | ...
63+
64+
subscriptions model =
65+
listen "user" UserLogout
66+
67+
Useful if the user logs out in another tab. We can then do something about it
68+
in this tab.
69+
70+
-}
71+
listen : String -> (String -> msg) -> Sub msg
72+
listen name tagger =
73+
subscription (Listen name tagger)
74+
75+
76+
subMap : (a -> b) -> MySub a -> MySub b
77+
subMap func sub =
78+
case sub of
79+
Listen url tagger ->
80+
Listen url (tagger >> func)
81+
82+
83+
84+
-- MANAGER
85+
86+
87+
type alias State msg =
88+
{ channels : ChannelsDict
89+
, subs : SubsDict msg
90+
}
91+
92+
93+
type alias ChannelsDict =
94+
Dict.Dict String BC.BroadcastChannel
95+
96+
97+
type alias SubsDict msg =
98+
Dict.Dict String (List (String -> msg))
99+
100+
101+
init : Task Never (State msg)
102+
init =
103+
Task.succeed (State Dict.empty Dict.empty)
104+
105+
106+
107+
-- HANDLE APP MESSAGES
108+
109+
110+
(&>) t1 t2 =
111+
Task.andThen (\_ -> t2) t1
112+
113+
114+
onEffects :
115+
Platform.Router msg Msg
116+
-> List (MyCmd msg)
117+
-> List (MySub msg)
118+
-> State msg
119+
-> Task Never (State msg)
120+
onEffects router cmds subs state =
121+
let
122+
sendMessages =
123+
sendMessagesHelp cmds state.channels
124+
125+
newSubs =
126+
buildSubDict subs Dict.empty
127+
128+
cleanup _ =
129+
let
130+
newEntries =
131+
Dict.map (\k v -> []) newSubs
132+
133+
leftStep name _ getNewChannels =
134+
getNewChannels
135+
|> Task.andThen
136+
(\newChannels ->
137+
open router name
138+
|> Task.andThen (\channel -> Task.succeed (Dict.insert name channel newChannels))
139+
)
140+
141+
bothStep name _ channel getNewChannels =
142+
Task.map (Dict.insert name channel) getNewChannels
143+
144+
rightStep name channel getNewChannels =
145+
close channel &> getNewChannels
146+
147+
collectNewChannels =
148+
Dict.merge leftStep bothStep rightStep newEntries state.channels (Task.succeed Dict.empty)
149+
in
150+
collectNewChannels
151+
|> Task.andThen (\newChannels -> Task.succeed (State newChannels newSubs))
152+
in
153+
sendMessages
154+
|> Task.andThen cleanup
155+
156+
157+
sendMessagesHelp : List (MyCmd msg) -> ChannelsDict -> Task Never ChannelsDict
158+
sendMessagesHelp cmds channelsDict =
159+
case cmds of
160+
[] ->
161+
Task.succeed channelsDict
162+
163+
(Send name msg) :: rest ->
164+
case Dict.get name channelsDict of
165+
Just channel ->
166+
BC.send channel msg
167+
&> sendMessagesHelp rest channelsDict
168+
169+
_ ->
170+
sendMessagesHelp rest channelsDict
171+
172+
173+
buildSubDict : List (MySub msg) -> SubsDict msg -> SubsDict msg
174+
buildSubDict subs dict =
175+
case subs of
176+
[] ->
177+
dict
178+
179+
(Listen name tagger) :: rest ->
180+
buildSubDict rest (Dict.update name (add tagger) dict)
181+
182+
183+
add : a -> Maybe (List a) -> Maybe (List a)
184+
add value maybeList =
185+
case maybeList of
186+
Nothing ->
187+
Just [ value ]
188+
189+
Just list ->
190+
Just (value :: list)
191+
192+
193+
194+
-- HANDLE SELF MESSAGES
195+
196+
197+
type Msg
198+
= Receive String String
199+
| Open String BC.BroadcastChannel
200+
201+
202+
onSelfMsg : Platform.Router msg Msg -> Msg -> State msg -> Task Never (State msg)
203+
onSelfMsg router selfMsg state =
204+
case selfMsg of
205+
Receive name str ->
206+
let
207+
sends =
208+
Dict.get name state.subs
209+
|> Maybe.withDefault []
210+
|> List.map (\tagger -> Platform.sendToApp router (tagger str))
211+
in
212+
Task.sequence sends &> Task.succeed state
213+
214+
Open name channel ->
215+
Task.succeed (updateChannel name channel state)
216+
217+
218+
updateChannel : String -> BC.BroadcastChannel -> State msg -> State msg
219+
updateChannel name channel state =
220+
{ state | channels = Dict.insert name channel state.channels }
221+
222+
223+
open : Platform.Router msg Msg -> String -> Task Never BC.BroadcastChannel
224+
open router name =
225+
let
226+
doOpen channel =
227+
Platform.sendToSelf router (Open name channel) |> Task.andThen (\_ -> Task.succeed channel)
228+
in
229+
BC.open name
230+
{ onMessage = \_ msg -> Platform.sendToSelf router (Receive name msg)
231+
}
232+
|> Task.andThen doOpen
233+
234+
235+
236+
-- CLOSE CONNECTIONS
237+
238+
239+
close : BC.BroadcastChannel -> Task Never ()
240+
close channel =
241+
BC.close channel

0 commit comments

Comments
 (0)