tangled
alpha
login
or
join now
eldridge.cam
/
cartography
0
fork
atom
Trading card city builder game?
0
fork
atom
overview
issues
pulls
pipelines
implement the pubsub
eldridge.cam
1 month ago
72cc018c
4a5f896d
+576
-21
16 changed files
expand all
collapse all
unified
split
.env.example
api
src
cartography_api
game_state.gleam
request.gleam
server
src
bus.gleam
db
sql
create_citizen.sql
create_tile.sql
get_card_type.sql
sql.gleam
handlers
debug_add_card_handler.gleam
pubsub.gleam
server
context.gleam
router.gleam
server.gleam
websocket
handler.gleam
state.gleam
src
lib
appserver
socket
SocketV1.svelte.ts
+2
.env.example
···
2
2
SHADOW_DATABASE_URL="postgres://postgres:postgres@localhost:5432/shadow"
3
3
ROOT_DATABASE_URL="postgres://postgres:postgres@localhost:5432/postgres"
4
4
5
5
+
WEBSOCKET_PROTOCOLS="json"
6
6
+
5
7
PUBLIC_SERVER_URL=http://localhost:8000
6
8
PUBLIC_SERVER_WS_URL=ws://localhost:8000
+1
api/src/cartography_api/game_state.gleam
···
17
17
}
18
18
19
19
pub type CardTypeId {
20
20
+
CardTypeId(id: String)
20
21
TileTypeId(id: String)
21
22
SpeciesId(id: String)
22
23
}
+4
-4
api/src/cartography_api/request.gleam
···
1
1
-
import cartography_api/game_state.{type FieldId}
1
1
+
import cartography_api/game_state.{type CardTypeId, type FieldId}
2
2
import cartography_api/internal/repr
3
3
import gleam/dynamic/decode
4
4
import gleam/json
···
19
19
ListFields
20
20
WatchField(field_id: FieldId)
21
21
Unsubscribe
22
22
-
DebugAddCard(card_id: String)
22
22
+
DebugAddCard(card_id: CardTypeId)
23
23
}
24
24
25
25
pub fn to_json(message: Message) -> json.Json {
···
34
34
|> repr.struct("WatchField")
35
35
Unsubscribe -> repr.struct(json.null(), "Unsubscribe")
36
36
DebugAddCard(card_id) ->
37
37
-
json.string(card_id)
37
37
+
json.string(card_id.id)
38
38
|> repr.struct("DebugAddCard")
39
39
}
40
40
json.object([#("id", json.string(uuid.to_string(id))), #("request", request)])
···
67
67
}
68
68
"DebugAddCard" -> {
69
69
use payload <- repr.struct_payload(decode.string)
70
70
-
decode.success(DebugAddCard(payload))
70
70
+
decode.success(DebugAddCard(game_state.CardTypeId(payload)))
71
71
}
72
72
_ -> {
73
73
decode.failure(Unsubscribe, "valid #tag")
+34
server/src/bus.gleam
···
1
1
+
import cartography_api/game_state.{type CardId}
2
2
+
import gleam/erlang/process
3
3
+
import gleam/otp/static_supervisor
4
4
+
import gleam/otp/supervision
5
5
+
import pubsub
6
6
+
7
7
+
pub opaque type Bus {
8
8
+
Bus(card_accounts_channel: process.Name(pubsub.Message(String, CardId)))
9
9
+
}
10
10
+
11
11
+
pub fn supervised() {
12
12
+
let card_accounts_channel = process.new_name("CardAccountsChannel")
13
13
+
14
14
+
let child_spec =
15
15
+
supervision.supervisor(fn() {
16
16
+
static_supervisor.new(static_supervisor.OneForOne)
17
17
+
|> static_supervisor.add(
18
18
+
pubsub.supervised(pubsub.named(card_accounts_channel)),
19
19
+
)
20
20
+
|> static_supervisor.start()
21
21
+
})
22
22
+
23
23
+
#(child_spec, Bus(card_accounts_channel:))
24
24
+
}
25
25
+
26
26
+
pub fn notify_card_account(bus: Bus, account_id: String, card_id: CardId) {
27
27
+
process.named_subject(bus.card_accounts_channel)
28
28
+
|> pubsub.broadcast(account_id, card_id)
29
29
+
}
30
30
+
31
31
+
pub fn on_card_account(bus: Bus, account_id: String) -> process.Subject(CardId) {
32
32
+
process.named_subject(bus.card_accounts_channel)
33
33
+
|> pubsub.subscribe(account_id)
34
34
+
}
+187
server/src/db/sql.gleam
···
46
46
|> pog.execute(db)
47
47
}
48
48
49
49
+
/// A row you get from running the `create_citizen` query
50
50
+
/// defined in `./src/db/sql/create_citizen.sql`.
51
51
+
///
52
52
+
/// > 🐿️ This type definition was generated automatically using v4.6.0 of the
53
53
+
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
54
54
+
///
55
55
+
pub type CreateCitizenRow {
56
56
+
CreateCitizenRow(card_id: Int, account_id: String)
57
57
+
}
58
58
+
59
59
+
/// Runs the `create_citizen` query
60
60
+
/// defined in `./src/db/sql/create_citizen.sql`.
61
61
+
///
62
62
+
/// > 🐿️ This function was generated automatically using v4.6.0 of
63
63
+
/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
64
64
+
///
65
65
+
pub fn create_citizen(
66
66
+
db: pog.Connection,
67
67
+
arg_1: String,
68
68
+
arg_2: String,
69
69
+
) -> Result(pog.Returned(CreateCitizenRow), pog.QueryError) {
70
70
+
let decoder = {
71
71
+
use card_id <- decode.field(0, decode.int)
72
72
+
use account_id <- decode.field(1, decode.string)
73
73
+
decode.success(CreateCitizenRow(card_id:, account_id:))
74
74
+
}
75
75
+
76
76
+
"WITH
77
77
+
cards_inserted AS (
78
78
+
INSERT INTO
79
79
+
cards (card_type_id)
80
80
+
VALUES
81
81
+
($1)
82
82
+
RETURNING
83
83
+
*
84
84
+
),
85
85
+
citizens_inserted AS (
86
86
+
INSERT INTO
87
87
+
citizens (id, species_id, name)
88
88
+
SELECT
89
89
+
card.id,
90
90
+
card.card_type_id,
91
91
+
''
92
92
+
FROM
93
93
+
cards_inserted card
94
94
+
)
95
95
+
INSERT INTO
96
96
+
card_accounts (card_id, account_id)
97
97
+
SELECT
98
98
+
card.id,
99
99
+
$2
100
100
+
FROM
101
101
+
cards_inserted card
102
102
+
RETURNING
103
103
+
*;
104
104
+
"
105
105
+
|> pog.query
106
106
+
|> pog.parameter(pog.text(arg_1))
107
107
+
|> pog.parameter(pog.text(arg_2))
108
108
+
|> pog.returning(decoder)
109
109
+
|> pog.execute(db)
110
110
+
}
111
111
+
112
112
+
/// A row you get from running the `create_tile` query
113
113
+
/// defined in `./src/db/sql/create_tile.sql`.
114
114
+
///
115
115
+
/// > 🐿️ This type definition was generated automatically using v4.6.0 of the
116
116
+
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
117
117
+
///
118
118
+
pub type CreateTileRow {
119
119
+
CreateTileRow(card_id: Int, account_id: String)
120
120
+
}
121
121
+
122
122
+
/// Runs the `create_tile` query
123
123
+
/// defined in `./src/db/sql/create_tile.sql`.
124
124
+
///
125
125
+
/// > 🐿️ This function was generated automatically using v4.6.0 of
126
126
+
/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
127
127
+
///
128
128
+
pub fn create_tile(
129
129
+
db: pog.Connection,
130
130
+
arg_1: String,
131
131
+
arg_2: String,
132
132
+
) -> Result(pog.Returned(CreateTileRow), pog.QueryError) {
133
133
+
let decoder = {
134
134
+
use card_id <- decode.field(0, decode.int)
135
135
+
use account_id <- decode.field(1, decode.string)
136
136
+
decode.success(CreateTileRow(card_id:, account_id:))
137
137
+
}
138
138
+
139
139
+
"WITH
140
140
+
cards_inserted AS (
141
141
+
INSERT INTO
142
142
+
cards (card_type_id)
143
143
+
VALUES
144
144
+
($1)
145
145
+
RETURNING
146
146
+
*
147
147
+
),
148
148
+
tiles_inserted AS (
149
149
+
INSERT INTO
150
150
+
tiles (id, tile_type_id, name)
151
151
+
SELECT
152
152
+
card.id,
153
153
+
card.card_type_id,
154
154
+
''
155
155
+
FROM
156
156
+
cards_inserted card
157
157
+
)
158
158
+
INSERT INTO
159
159
+
card_accounts (card_id, account_id)
160
160
+
SELECT
161
161
+
card.id,
162
162
+
$2
163
163
+
FROM
164
164
+
cards_inserted card
165
165
+
RETURNING
166
166
+
*;
167
167
+
"
168
168
+
|> pog.query
169
169
+
|> pog.parameter(pog.text(arg_1))
170
170
+
|> pog.parameter(pog.text(arg_2))
171
171
+
|> pog.returning(decoder)
172
172
+
|> pog.execute(db)
173
173
+
}
174
174
+
49
175
/// A row you get from running the `get_account` query
50
176
/// defined in `./src/db/sql/get_account.sql`.
51
177
///
···
77
203
accounts
78
204
WHERE
79
205
id = $1
206
206
+
"
207
207
+
|> pog.query
208
208
+
|> pog.parameter(pog.text(arg_1))
209
209
+
|> pog.returning(decoder)
210
210
+
|> pog.execute(db)
211
211
+
}
212
212
+
213
213
+
/// A row you get from running the `get_card_type` query
214
214
+
/// defined in `./src/db/sql/get_card_type.sql`.
215
215
+
///
216
216
+
/// > 🐿️ This type definition was generated automatically using v4.6.0 of the
217
217
+
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
218
218
+
///
219
219
+
pub type GetCardTypeRow {
220
220
+
GetCardTypeRow(id: String, card_set_id: String, class: CardClass)
221
221
+
}
222
222
+
223
223
+
/// Runs the `get_card_type` query
224
224
+
/// defined in `./src/db/sql/get_card_type.sql`.
225
225
+
///
226
226
+
/// > 🐿️ This function was generated automatically using v4.6.0 of
227
227
+
/// > the [squirrel package](https://github.com/giacomocavalieri/squirrel).
228
228
+
///
229
229
+
pub fn get_card_type(
230
230
+
db: pog.Connection,
231
231
+
arg_1: String,
232
232
+
) -> Result(pog.Returned(GetCardTypeRow), pog.QueryError) {
233
233
+
let decoder = {
234
234
+
use id <- decode.field(0, decode.string)
235
235
+
use card_set_id <- decode.field(1, decode.string)
236
236
+
use class <- decode.field(2, card_class_decoder())
237
237
+
decode.success(GetCardTypeRow(id:, card_set_id:, class:))
238
238
+
}
239
239
+
240
240
+
"SELECT
241
241
+
*
242
242
+
FROM
243
243
+
card_types
244
244
+
WHERE
245
245
+
id = $1;
80
246
"
81
247
|> pog.query
82
248
|> pog.parameter(pog.text(arg_1))
···
206
372
|> pog.returning(decoder)
207
373
|> pog.execute(db)
208
374
}
375
375
+
376
376
+
// --- Enums -------------------------------------------------------------------
377
377
+
378
378
+
/// Corresponds to the Postgres `card_class` enum.
379
379
+
///
380
380
+
/// > 🐿️ This type definition was generated automatically using v4.6.0 of the
381
381
+
/// > [squirrel package](https://github.com/giacomocavalieri/squirrel).
382
382
+
///
383
383
+
pub type CardClass {
384
384
+
Citizen
385
385
+
Tile
386
386
+
}
387
387
+
388
388
+
fn card_class_decoder() -> decode.Decoder(CardClass) {
389
389
+
use card_class <- decode.then(decode.string)
390
390
+
case card_class {
391
391
+
"citizen" -> decode.success(Citizen)
392
392
+
"tile" -> decode.success(Tile)
393
393
+
_ -> decode.failure(Citizen, "CardClass")
394
394
+
}
395
395
+
}
+28
server/src/db/sql/create_citizen.sql
···
1
1
+
WITH
2
2
+
cards_inserted AS (
3
3
+
INSERT INTO
4
4
+
cards (card_type_id)
5
5
+
VALUES
6
6
+
($1)
7
7
+
RETURNING
8
8
+
*
9
9
+
),
10
10
+
citizens_inserted AS (
11
11
+
INSERT INTO
12
12
+
citizens (id, species_id, name)
13
13
+
SELECT
14
14
+
card.id,
15
15
+
card.card_type_id,
16
16
+
''
17
17
+
FROM
18
18
+
cards_inserted card
19
19
+
)
20
20
+
INSERT INTO
21
21
+
card_accounts (card_id, account_id)
22
22
+
SELECT
23
23
+
card.id,
24
24
+
$2
25
25
+
FROM
26
26
+
cards_inserted card
27
27
+
RETURNING
28
28
+
*;
+28
server/src/db/sql/create_tile.sql
···
1
1
+
WITH
2
2
+
cards_inserted AS (
3
3
+
INSERT INTO
4
4
+
cards (card_type_id)
5
5
+
VALUES
6
6
+
($1)
7
7
+
RETURNING
8
8
+
*
9
9
+
),
10
10
+
tiles_inserted AS (
11
11
+
INSERT INTO
12
12
+
tiles (id, tile_type_id, name)
13
13
+
SELECT
14
14
+
card.id,
15
15
+
card.card_type_id,
16
16
+
''
17
17
+
FROM
18
18
+
cards_inserted card
19
19
+
)
20
20
+
INSERT INTO
21
21
+
card_accounts (card_id, account_id)
22
22
+
SELECT
23
23
+
card.id,
24
24
+
$2
25
25
+
FROM
26
26
+
cards_inserted card
27
27
+
RETURNING
28
28
+
*;
+6
server/src/db/sql/get_card_type.sql
···
1
1
+
SELECT
2
2
+
*
3
3
+
FROM
4
4
+
card_types
5
5
+
WHERE
6
6
+
id = $1;
+44
server/src/handlers/debug_add_card_handler.gleam
···
1
1
+
import bus
2
2
+
import cartography_api/game_state
3
3
+
import db/rows
4
4
+
import db/sql
5
5
+
import gleam/result
6
6
+
import gleam/string
7
7
+
import mist
8
8
+
import websocket/state
9
9
+
10
10
+
pub fn handle(
11
11
+
st: state.State,
12
12
+
card_type_id: game_state.CardTypeId,
13
13
+
) -> Result(mist.Next(state.State, _msg), String) {
14
14
+
use account_id <- state.account_id(st)
15
15
+
{
16
16
+
let assert Ok(card_type) =
17
17
+
state.db_connection(st)
18
18
+
|> sql.get_card_type(card_type_id.id)
19
19
+
use card_type <- rows.one(card_type)
20
20
+
21
21
+
use card_id <- result.try(case card_type.class {
22
22
+
sql.Citizen -> {
23
23
+
let assert Ok(citizen) =
24
24
+
state.db_connection(st)
25
25
+
|> sql.create_citizen(account_id, card_type.id)
26
26
+
use citizen <- rows.one(citizen)
27
27
+
Ok(game_state.CitizenId(citizen.card_id))
28
28
+
}
29
29
+
sql.Tile -> {
30
30
+
let assert Ok(tile) =
31
31
+
state.db_connection(st)
32
32
+
|> sql.create_tile(account_id, card_type.id)
33
33
+
use tile <- rows.one(tile)
34
34
+
Ok(game_state.TileId(tile.card_id))
35
35
+
}
36
36
+
})
37
37
+
38
38
+
state.bus(st)
39
39
+
|> bus.notify_card_account(account_id, card_id)
40
40
+
41
41
+
Ok(mist.continue(st))
42
42
+
}
43
43
+
|> result.map_error(string.inspect)
44
44
+
}
+177
server/src/pubsub.gleam
···
1
1
+
import gleam/dict.{type Dict}
2
2
+
import gleam/erlang/process.{type Down, type Monitor, type Pid, type Subject}
3
3
+
import gleam/otp/actor
4
4
+
import gleam/otp/supervision
5
5
+
import gleam/result
6
6
+
import gleam/set.{type Set}
7
7
+
8
8
+
pub opaque type Message(channel, message) {
9
9
+
Subscribe(channel, Subject(message))
10
10
+
Unsubscribe(Subject(message))
11
11
+
Hangup(Down)
12
12
+
Broadcast(channel, message)
13
13
+
}
14
14
+
15
15
+
pub type PubSub(channel, message)
16
16
+
17
17
+
type State(channel, message) {
18
18
+
State(
19
19
+
channels: Dict(channel, Set(Subject(message))),
20
20
+
monitors: Dict(Pid, #(Monitor, Set(Subject(message)))),
21
21
+
)
22
22
+
}
23
23
+
24
24
+
pub fn start(name: Name(channel, message)) {
25
25
+
actor.new_with_initialiser(10, fn(sub) {
26
26
+
let selector =
27
27
+
process.new_selector()
28
28
+
|> process.select(sub)
29
29
+
|> process.select_monitors(Hangup)
30
30
+
31
31
+
actor.initialised(State(channels: dict.new(), monitors: dict.new()))
32
32
+
|> actor.selecting(selector)
33
33
+
|> Ok()
34
34
+
})
35
35
+
|> actor.named(name)
36
36
+
|> actor.on_message(on_message)
37
37
+
|> actor.start()
38
38
+
}
39
39
+
40
40
+
type Name(channel, message) =
41
41
+
process.Name(Message(channel, message))
42
42
+
43
43
+
pub opaque type Config(channel, message) {
44
44
+
Config(name: Name(channel, message))
45
45
+
}
46
46
+
47
47
+
pub fn supervised(config: Config(channel, message)) {
48
48
+
supervision.supervisor(fn() { start(config.name) })
49
49
+
}
50
50
+
51
51
+
pub fn named(name: Name(channel, message)) {
52
52
+
Config(name:)
53
53
+
}
54
54
+
55
55
+
fn on_message(
56
56
+
state: State(channel, message),
57
57
+
message: Message(channel, message),
58
58
+
) -> actor.Next(State(channel, message), Message(channel, message)) {
59
59
+
case message {
60
60
+
Broadcast(channel, message) -> {
61
61
+
dict.get(state.channels, channel)
62
62
+
|> result.lazy_unwrap(set.new)
63
63
+
|> set.each(process.send(_, message))
64
64
+
actor.continue(state)
65
65
+
}
66
66
+
Subscribe(channel, subject) ->
67
67
+
handle_subscribe(state, channel, subject)
68
68
+
|> actor.continue()
69
69
+
Unsubscribe(subject) ->
70
70
+
handle_unsubscribe(state, subject)
71
71
+
|> actor.continue()
72
72
+
Hangup(down) ->
73
73
+
handle_hangup(state, down)
74
74
+
|> actor.continue()
75
75
+
}
76
76
+
}
77
77
+
78
78
+
fn remove_subject(state: State(channel, message), subject: Subject(message)) {
79
79
+
let assert Ok(pid) = process.subject_owner(subject)
80
80
+
let assert Ok(#(monitor, subjects)) = dict.get(state.monitors, pid)
81
81
+
let subjects = set.delete(subjects, subject)
82
82
+
let monitors = case set.is_empty(subjects) {
83
83
+
True -> {
84
84
+
process.demonitor_process(monitor)
85
85
+
dict.delete(state.monitors, pid)
86
86
+
}
87
87
+
False -> dict.insert(state.monitors, pid, #(monitor, subjects))
88
88
+
}
89
89
+
State(..state, monitors:)
90
90
+
}
91
91
+
92
92
+
fn remove_listener(state: State(channel, message), subject: Subject(message)) {
93
93
+
let channels =
94
94
+
dict.map_values(state.channels, fn(_, subs) { set.delete(subs, subject) })
95
95
+
State(..state, channels:)
96
96
+
}
97
97
+
98
98
+
fn add_listener(
99
99
+
state: State(channel, message),
100
100
+
channel: channel,
101
101
+
subject: Subject(message),
102
102
+
) {
103
103
+
let channels =
104
104
+
state.channels
105
105
+
|> dict.get(channel)
106
106
+
|> result.lazy_unwrap(set.new)
107
107
+
|> set.insert(subject)
108
108
+
|> dict.insert(state.channels, channel, _)
109
109
+
State(..state, channels:)
110
110
+
}
111
111
+
112
112
+
fn add_monitor(state: State(channel, message), subject: Subject(message)) {
113
113
+
let assert Ok(pid) = process.subject_owner(subject)
114
114
+
let monitors = case dict.get(state.monitors, pid) {
115
115
+
Ok(#(monitor, subjects)) ->
116
116
+
dict.insert(state.monitors, pid, #(monitor, set.insert(subjects, subject)))
117
117
+
Error(Nil) ->
118
118
+
dict.insert(state.monitors, pid, #(
119
119
+
process.monitor(pid),
120
120
+
set.new() |> set.insert(subject),
121
121
+
))
122
122
+
}
123
123
+
State(..state, monitors:)
124
124
+
}
125
125
+
126
126
+
fn remove_monitor(state: State(channel, message), pid: Pid) {
127
127
+
State(..state, monitors: dict.delete(state.monitors, pid))
128
128
+
}
129
129
+
130
130
+
fn handle_subscribe(
131
131
+
state: State(channel, message),
132
132
+
channel: channel,
133
133
+
subject: Subject(message),
134
134
+
) -> State(channel, message) {
135
135
+
state
136
136
+
|> add_listener(channel, subject)
137
137
+
|> add_monitor(subject)
138
138
+
}
139
139
+
140
140
+
fn handle_unsubscribe(
141
141
+
state: State(channel, message),
142
142
+
subject: Subject(message),
143
143
+
) -> State(channel, message) {
144
144
+
state
145
145
+
|> remove_listener(subject)
146
146
+
|> remove_subject(subject)
147
147
+
}
148
148
+
149
149
+
fn handle_hangup(
150
150
+
state: State(channel, message),
151
151
+
down: Down,
152
152
+
) -> State(channel, message) {
153
153
+
case down {
154
154
+
process.ProcessDown(monitor, pid, _reason) -> {
155
155
+
process.demonitor_process(monitor)
156
156
+
let assert Ok(#(_, subjects)) = dict.get(state.monitors, pid)
157
157
+
subjects
158
158
+
|> set.fold(state, remove_listener)
159
159
+
|> remove_monitor(pid)
160
160
+
}
161
161
+
process.PortDown(..) -> panic as "unreachable"
162
162
+
}
163
163
+
}
164
164
+
165
165
+
pub fn broadcast(
166
166
+
pubsub: Subject(Message(channel, message)),
167
167
+
channel: channel,
168
168
+
message: message,
169
169
+
) {
170
170
+
process.send(pubsub, Broadcast(channel, message))
171
171
+
}
172
172
+
173
173
+
pub fn subscribe(pubsub: Subject(Message(channel, message)), channel: channel) {
174
174
+
let subject = process.new_subject()
175
175
+
process.send(pubsub, Subscribe(channel, subject))
176
176
+
subject
177
177
+
}
+11
-7
server/src/server.gleam
···
1
1
+
import bus
1
2
import envoy
2
3
import gleam/erlang/process
3
4
import gleam/int
4
4
-
import gleam/otp/static_supervisor as sup
5
5
+
import gleam/otp/static_supervisor
5
6
import gleam/result
6
7
import mist
7
8
import palabres
···
30
31
|> pog.pool_size(10)
31
32
|> pog.supervised()
32
33
33
33
-
let context = context.Context(db_name)
34
34
+
let #(bus_process, bus_handles) = bus.supervised()
35
35
+
36
36
+
let context = context.Context(db_name, bus_handles)
34
37
let server =
35
35
-
mist.new(fn(req) { router.handler(req, context) })
38
38
+
mist.new(router.handler(_, context))
36
39
|> mist.port(port)
37
40
|> mist.supervised()
38
41
39
42
let assert Ok(_) =
40
40
-
sup.new(sup.OneForOne)
41
41
-
|> sup.add(database)
42
42
-
|> sup.add(server)
43
43
-
|> sup.start()
43
43
+
static_supervisor.new(static_supervisor.OneForOne)
44
44
+
|> static_supervisor.add(database)
45
45
+
|> static_supervisor.add(server)
46
46
+
|> static_supervisor.add(bus_process)
47
47
+
|> static_supervisor.start()
44
48
45
49
process.sleep_forever()
46
50
}
+2
-1
server/src/server/context.gleam
···
1
1
+
import bus
1
2
import gleam/erlang/process.{type Name}
2
3
import pog
3
4
4
5
pub type Context {
5
5
-
Context(db: Name(pog.Message))
6
6
+
Context(db: Name(pog.Message), bus: bus.Bus)
6
7
}
+8
-1
server/src/server/router.gleam
···
7
7
8
8
pub fn handler(req: request.Request(mist.Connection), context: Context) {
9
9
case request.path_segments(req) {
10
10
-
["websocket"] -> handler.start(req, context)
10
10
+
["websocket"] -> {
11
11
+
case handler.start(req, context) {
12
12
+
Ok(response) -> response
13
13
+
Error(_) ->
14
14
+
response.new(400)
15
15
+
|> response.set_body(mist.Bytes(bytes_tree.new()))
16
16
+
}
17
17
+
}
11
18
_ ->
12
19
response.new(404)
13
20
|> response.set_body(mist.Bytes(bytes_tree.new()))
+38
-7
server/src/websocket/handler.gleam
···
1
1
import cartography_api/request
2
2
+
import envoy
2
3
import gleam/http/request as http
4
4
+
import gleam/http/response
5
5
+
import gleam/list
6
6
+
import gleam/result
7
7
+
import gleam/string
3
8
import handlers/authenticate_handler
9
9
+
import handlers/debug_add_card_handler
4
10
import handlers/list_fields_handler
5
11
import json_websocket
6
12
import mist.{type WebsocketConnection}
···
21
27
request.WatchField(_) -> {
22
28
todo
23
29
}
24
24
-
request.DebugAddCard(_) -> {
25
25
-
todo
26
26
-
}
30
30
+
request.DebugAddCard(card_id) ->
31
31
+
debug_add_card_handler.handle(state, card_id)
27
32
request.Unsubscribe -> {
28
33
todo
29
34
}
···
41
46
}
42
47
43
48
pub fn start(request: http.Request(mist.Connection), context: Context) {
44
44
-
state.new(context)
45
45
-
|> json_websocket.new()
46
46
-
|> json_websocket.message(request.decoder(), handle_message)
47
47
-
|> json_websocket.start(request)
49
49
+
use protocol <- result.try(http.get_header(request, "sec-websocket-protocol"))
50
50
+
start_with_protocol(string.split(protocol, on: ","), request, context)
51
51
+
}
52
52
+
53
53
+
fn start_with_protocol(
54
54
+
protocol: List(String),
55
55
+
request: http.Request(mist.Connection),
56
56
+
context: Context,
57
57
+
) {
58
58
+
let supported =
59
59
+
envoy.get("WEBSOCKET_PROTOCOLS")
60
60
+
|> result.unwrap("json")
61
61
+
|> string.split(on: ",")
62
62
+
63
63
+
let allow_json = list.contains(supported, "json")
64
64
+
65
65
+
case protocol {
66
66
+
[] -> Error(Nil)
67
67
+
["v1-json.cartography.app", ..] if allow_json ->
68
68
+
state.new(context)
69
69
+
|> json_websocket.new()
70
70
+
|> json_websocket.message(request.decoder(), handle_message)
71
71
+
|> json_websocket.start(request)
72
72
+
|> response.set_header(
73
73
+
"sec-websocket-protocol",
74
74
+
"v1-json.cartography.app",
75
75
+
)
76
76
+
|> Ok()
77
77
+
[_, ..rest] -> start_with_protocol(rest, request, context)
78
78
+
}
48
79
}
+5
server/src/websocket/state.gleam
···
1
1
+
import bus
1
2
import gleam/dict
2
3
import gleam/option
3
4
import pog
···
31
32
32
33
pub fn db_connection(state: State) -> pog.Connection {
33
34
pog.named_connection(state.context.db)
35
35
+
}
36
36
+
37
37
+
pub fn bus(state: State) -> bus.Bus {
38
38
+
state.context.bus
34
39
}
35
40
36
41
pub fn add_listener(
+1
-1
src/lib/appserver/socket/SocketV1.svelte.ts
···
24
24
}
25
25
26
26
export class SocketV1 extends ReactiveEventTarget<SocketV1EventMap> {
27
27
-
static readonly PROTOCOL = ["v1.cartography.app"];
27
27
+
static readonly PROTOCOL = ["v1-json.cartography.app", "v1-messagepack.cartography.app"];
28
28
29
29
#socket: WebSocket;
30
30
#url: string;