1
1
import type { ServerWebSocket } from "bun" ;
2
2
import type { ClientData , Departure } from "./types" ;
3
3
4
- import { v4 as uuid } from "uuid" ;
5
4
import { group } from "radash" ;
6
5
7
6
import { STOP_IDS_HEADER , INTERVAL } from "./server/server.const" ;
8
7
import { fetchApiData } from "./fetch-metro/fetch-metro" ;
9
8
import { StopIDsSchema , SubscribeSchema , type ApiResponse } from "./schemas" ;
10
- import { getErrorResponse , getParsedDeparture } from "./server/server.utils" ;
9
+ import { getParsedDeparture } from "./server/server.utils" ;
11
10
12
11
if ( ! process . env . GOLEMIO_API_KEY ) {
13
12
throw new Error ( "GOLEMIO_API_KEY is not set in .env" ) ;
@@ -20,23 +19,41 @@ const wsByClientID = new Map<string, ServerWebSocket<ClientData>>();
20
19
21
20
const departuresByStopID = new Map < string , Departure [ ] > ( ) ;
22
21
23
- const getAllSubscribedStopIDs = ( ) : string [ ] => {
22
+ /**
23
+ * If clientID is provided, fetch only the stopIDs that the client is subscribed to
24
+ * and that haven't been fetched yet.
25
+ *
26
+ * Otherwise, refetch all stopIDs that are subscribed by any client.
27
+ */
28
+ const getStopIDsToFetch = ( clientID ?: string ) : string [ ] => {
29
+ if ( clientID !== undefined ) {
30
+ const subscribedStopIDs = subscribedStopIDsByClientID . get ( clientID ) ?? [ ] ;
31
+ const notFetchedStopIDs = subscribedStopIDs . filter (
32
+ ( stopID ) => ! departuresByStopID . has ( stopID )
33
+ ) ;
34
+ return notFetchedStopIDs ;
35
+ }
36
+
24
37
const stopIDsByClientIDMapValues = subscribedStopIDsByClientID . values ( ) ;
25
- return Array . from ( stopIDsByClientIDMapValues ) . flat ( ) ;
38
+ return [ ...stopIDsByClientIDMapValues ] . flat ( ) ;
39
+ } ;
40
+
41
+ /**
42
+ * Return only the data that the client is subscribed to
43
+ * as stringified object
44
+ */
45
+ const getStringifiedDataForClientID = ( clientID : string ) : string => {
46
+ const clientsStopIDs = subscribedStopIDsByClientID . get ( clientID ) ! ;
47
+ const dataForClientEntries = clientsStopIDs . map ( ( stopID ) => [
48
+ stopID ,
49
+ departuresByStopID . get ( stopID ) ,
50
+ ] ) ;
51
+ const dataForClient = Object . fromEntries ( dataForClientEntries ) ;
52
+ return JSON . stringify ( dataForClient ) ;
26
53
} ;
27
54
28
55
const fetchData = async ( clientID ?: string ) => {
29
- /**
30
- * If clientID is provided, fetch only the stopIDs that the client is subscribed to
31
- * and that haven't been fetched yet.
32
- *
33
- * Otherwise, refetch all stopIDs that are subscribed by any client.
34
- */
35
- const stopIDsToFetch : string [ ] = clientID
36
- ? subscribedStopIDsByClientID
37
- . get ( clientID ) !
38
- . filter ( ( stopID ) => ! departuresByStopID . has ( stopID ) )
39
- : getAllSubscribedStopIDs ( ) ;
56
+ const stopIDsToFetch : string [ ] = getStopIDsToFetch ( clientID ) ;
40
57
41
58
const res = await fetchApiData ( stopIDsToFetch ) ;
42
59
/**
@@ -61,65 +78,47 @@ const fetchData = async (clientID?: string) => {
61
78
} ) ;
62
79
}
63
80
64
- /**
65
- * Return only the data that the client is subscribed to
66
- * as stringified object
67
- */
68
- const getStringifiedDataForClientID = ( clientID : string ) : string => {
69
- const stopIDsSubscribedByClient =
70
- subscribedStopIDsByClientID . get ( clientID ) ! ;
71
- const dataForClient = Object . fromEntries (
72
- stopIDsSubscribedByClient . map ( ( stopID ) => [
73
- stopID ,
74
- departuresByStopID . get ( stopID ) ,
75
- ] )
76
- ) ;
77
- return JSON . stringify ( dataForClient ) ;
78
- } ;
79
-
80
- /**
81
- * If clientID is provided, send data to the client
82
- */
81
+ // If clientID is provided, send data only to the client
83
82
if ( clientID ) {
84
83
const ws = wsByClientID . get ( clientID ) ! ;
85
-
86
84
ws . send ( getStringifiedDataForClientID ( clientID ) ) ;
87
-
88
85
return ;
89
86
}
90
87
91
- /**
92
- * If clientID is not provided, send data to all clients
93
- */
88
+ // If clientID is not provided, send data to all clients
94
89
wsByClientID . forEach ( ( ws , clientID ) =>
95
90
ws . send ( getStringifiedDataForClientID ( clientID ) )
96
91
) ;
97
92
} ;
98
93
99
94
const server = Bun . serve < ClientData > ( {
100
95
fetch ( req , server ) {
101
- const stopIDsHeaderRaw = req . headers . get ( STOP_IDS_HEADER ) ;
102
- if ( ! stopIDsHeaderRaw )
103
- return getErrorResponse ( `"${ STOP_IDS_HEADER } " header is missing` ) ;
104
-
105
- let StopIDsHeaderParsed : unknown ;
106
96
try {
107
- StopIDsHeaderParsed = JSON . parse ( stopIDsHeaderRaw ) ;
108
- } catch ( error ) {
109
- return getErrorResponse ( `"${ STOP_IDS_HEADER } " header ${ error } ` ) ;
110
- }
97
+ const stopIDsHeaderRaw = req . headers . get ( STOP_IDS_HEADER ) ;
98
+ if ( ! stopIDsHeaderRaw ) {
99
+ throw `"${ STOP_IDS_HEADER } " header is missing` ;
100
+ }
111
101
112
- const res = StopIDsSchema . safeParse ( StopIDsHeaderParsed ) ;
113
- if ( ! res . success )
114
- return getErrorResponse (
115
- `"${ STOP_IDS_HEADER } " error: ${ res . error . errors [ 0 ] . message } `
116
- ) ;
102
+ const StopIDsHeaderParsed : unknown = JSON . parse ( stopIDsHeaderRaw ) ;
103
+ const res = StopIDsSchema . safeParse ( StopIDsHeaderParsed ) ;
104
+ if ( ! res . success ) {
105
+ throw (
106
+ `Invalid "${ STOP_IDS_HEADER } " header: ` +
107
+ JSON . stringify ( res . error . errors [ 0 ] . message )
108
+ ) ;
109
+ }
117
110
118
- const clientID = uuid ( ) ;
119
- subscribedStopIDsByClientID . set ( clientID , res . data ) ;
120
- const success = server . upgrade ( req , { data : { clientID } } ) ;
111
+ const clientID = crypto . randomUUID ( ) ;
112
+ subscribedStopIDsByClientID . set ( clientID , res . data ) ;
121
113
122
- if ( ! success ) return getErrorResponse ( "Failed to upgrade connection" ) ;
114
+ const success = server . upgrade ( req , { data : { clientID } } ) ;
115
+ if ( ! success ) throw "Failed to upgrade connection" ;
116
+ } catch ( e ) {
117
+ return new Response ( String ( e ) , {
118
+ status : 500 ,
119
+ headers : [ [ "error" , String ( e ) ] ] , // Postman doesn't show response body when testing websocket
120
+ } ) ;
121
+ }
123
122
} ,
124
123
websocket : {
125
124
open ( ws ) {
@@ -135,26 +134,17 @@ const server = Bun.serve<ClientData>({
135
134
intervalId = intervalObj [ Symbol . toPrimitive ] ( ) ;
136
135
} ,
137
136
message ( ws , message ) {
138
- if ( typeof message !== "string" ) {
139
- ws . close ( 1011 , "Message has to be string" ) ;
140
- return ;
141
- }
142
-
143
- let StopIDsHeaderParsed : unknown ;
144
137
try {
145
- StopIDsHeaderParsed = JSON . parse ( message ) ;
146
- } catch ( error ) {
147
- ws . close ( 1011 , String ( error ) ) ;
148
- return ;
149
- }
138
+ if ( typeof message !== "string" ) throw "Message has to be string" ;
150
139
151
- const res = SubscribeSchema . safeParse ( StopIDsHeaderParsed ) ;
152
- if ( ! res . success ) {
153
- ws . close ( 1011 , res . error . errors [ 0 ] . message ) ;
154
- return ;
155
- }
140
+ var StopIDsHeaderParsed : unknown = JSON . parse ( message ) ;
141
+ const res = SubscribeSchema . safeParse ( StopIDsHeaderParsed ) ;
142
+ if ( ! res . success ) throw JSON . stringify ( res . error . errors [ 0 ] . message ) ;
156
143
157
- subscribedStopIDsByClientID . set ( ws . data . clientID , res . data . subscribe ) ;
144
+ subscribedStopIDsByClientID . set ( ws . data . clientID , res . data . subscribe ) ;
145
+ } catch ( e ) {
146
+ ws . close ( 1011 , String ( e ) ) ;
147
+ }
158
148
} ,
159
149
close ( ws ) {
160
150
const clientID = ws . data . clientID ;
0 commit comments