comparison seobeo/s_web.c @ 257:609d3c6aff4e

[seobeo] Add persistent SSE streams Co-authored-by: Copilot <[email protected]>
author MrJuneJune <me@mrjunejune.com>
date Tue, 04 Aug 2026 16:49:11 -0700
parents 117c4d53c9a4
children 1f9877b637e9
comparison
equal deleted inserted replaced
256:30c2196d03d4 257:609d3c6aff4e
134 found = TRUE; 134 found = TRUE;
135 break; 135 break;
136 } 136 }
137 token = strtok(NULL, ", \t"); 137 token = strtok(NULL, ", \t");
138 } 138 }
139 free(copy); 139 Dowa_Free(copy);
140 return found; 140 return found;
141 } 141 }
142 142
143 // Returns TRUE if connection should be kept alive, FALSE if it should be closed 143 // Returns TRUE if connection should be kept alive, FALSE if it should be closed
144 boolean Seobeo_Web_ClientHandle_Request(Seobeo_Handle *p_cli_handle, 144 boolean Seobeo_Web_ClientHandle_Request(Seobeo_Handle *p_cli_handle,
241 return FALSE; // WebSocket takes over, don't keep-alive in HTTP sense 241 return FALSE; // WebSocket takes over, don't keep-alive in HTTP sense
242 } 242 }
243 #endif 243 #endif
244 244
245 // --- Try to match streaming route first --- 245 // --- Try to match streaming route first ---
246 Seobeo_SSE_Handler sse_handler = Seobeo_Router_Find_SSE_Handler(
247 method,
248 path,
249 &p_req_map,
250 p_request_arena);
251 if (sse_handler != NULL)
252 {
253 Seobeo_SSE_Stream *p_stream = Seobeo_SSE_Server_Attach(
254 p_cli_handle,
255 path);
256 if (p_stream)
257 {
258 sse_handler(p_stream, p_req_map, p_response_arena);
259 should_keep_alive = Seobeo_SSE_Is_Open(p_stream);
260 }
261 else
262 {
263 should_keep_alive = FALSE;
264 }
265 goto clean_up_arenas;
266 }
267
268 // --- Try to match generic streaming route ---
246 Seobeo_Stream_Handler stream_handler = Seobeo_Router_Find_Stream_Handler(method, path, &p_req_map, p_request_arena); 269 Seobeo_Stream_Handler stream_handler = Seobeo_Router_Find_Stream_Handler(method, path, &p_req_map, p_request_arena);
247 if (stream_handler != NULL) 270 if (stream_handler != NULL)
248 { 271 {
249 stream_handler(p_cli_handle, p_req_map, p_response_arena); 272 stream_handler(p_cli_handle, p_req_map, p_response_arena);
250 goto clean_up_arenas; 273 goto clean_up_arenas;
354 (uint32)strlen(p_response_header)); 377 (uint32)strlen(p_response_header));
355 Seobeo_Handle_Queue(p_cli_handle, 378 Seobeo_Handle_Queue(p_cli_handle,
356 (const uint8*)file_content, 379 (const uint8*)file_content,
357 (uint32)body_size); 380 (uint32)body_size);
358 Seobeo_Handle_Flush(p_cli_handle); 381 Seobeo_Handle_Flush(p_cli_handle);
359 free(file_content); 382 Dowa_Free(file_content);
360 } 383 }
361 else 384 else
362 { 385 {
363 Seobeo_Web_Header_Generate_KeepAlive(p_response_header, 386 Seobeo_Web_Header_Generate_KeepAlive(p_response_header,
364 HTTP_NOT_FOUND, 387 HTTP_NOT_FOUND,
688 struct Seobeo_Route_Struct { 711 struct Seobeo_Route_Struct {
689 char *method; // "GET", "POST", "PUT", "DELETE" 712 char *method; // "GET", "POST", "PUT", "DELETE"
690 char *path_pattern; // "/v1/users/:id/posts/:post_id" 713 char *path_pattern; // "/v1/users/:id/posts/:post_id"
691 Seobeo_Route_Handler handler; 714 Seobeo_Route_Handler handler;
692 Seobeo_Stream_Handler stream_handler; // For streaming responses 715 Seobeo_Stream_Handler stream_handler; // For streaming responses
716 Seobeo_SSE_Handler sse_handler;
693 717
694 // Pre-parsed path segments for efficient matching 718 // Pre-parsed path segments for efficient matching
695 char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"] 719 char **path_segments; // ["v1", "users", ":id", "posts", ":post_id"]
696 boolean *is_param; // [false, false, true, false, true] 720 boolean *is_param; // [false, false, true, false, true]
697 size_t segment_count; 721 size_t segment_count;
710 734
711 route.method = strdup(method); 735 route.method = strdup(method);
712 route.path_pattern = strdup(path_pattern); 736 route.path_pattern = strdup(path_pattern);
713 route.handler = handler; 737 route.handler = handler;
714 route.stream_handler = NULL; 738 route.stream_handler = NULL;
739 route.sse_handler = NULL;
715 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); 740 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
716 route.segment_count = Dowa_Array_Length(route.path_segments); 741 route.segment_count = Dowa_Array_Length(route.path_segments);
717 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); 742 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
718 743
719 for (size_t i = 0; i < route.segment_count; i++) 744 for (size_t i = 0; i < route.segment_count; i++)
728 753
729 route.method = strdup(method); 754 route.method = strdup(method);
730 route.path_pattern = strdup(path_pattern); 755 route.path_pattern = strdup(path_pattern);
731 route.handler = NULL; 756 route.handler = NULL;
732 route.stream_handler = handler; 757 route.stream_handler = handler;
758 route.sse_handler = NULL;
733 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL); 759 route.path_segments = Dowa_String_Split(path_pattern, "/", strlen(path_pattern), 1, NULL);
734 route.segment_count = Dowa_Array_Length(route.path_segments); 760 route.segment_count = Dowa_Array_Length(route.path_segments);
735 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count); 761 route.is_param = (boolean*)malloc(sizeof(boolean) * route.segment_count);
762
763 for (size_t i = 0; i < route.segment_count; i++)
764 route.is_param[i] = (route.path_segments[i][0] == ':');
765
766 Dowa_Array_Push(g_routes, route);
767 }
768
769 void Seobeo_Router_Register_SSE(
770 const char *path_pattern,
771 Seobeo_SSE_Handler handler)
772 {
773 Seobeo_Route route = {0};
774
775 route.method = strdup("GET");
776 route.path_pattern = strdup(path_pattern);
777 route.handler = NULL;
778 route.stream_handler = NULL;
779 route.sse_handler = handler;
780 route.path_segments = Dowa_String_Split(
781 path_pattern,
782 "/",
783 strlen(path_pattern),
784 1,
785 NULL);
786 route.segment_count = Dowa_Array_Length(route.path_segments);
787 route.is_param = (boolean*)malloc(
788 sizeof(boolean) * route.segment_count);
736 789
737 for (size_t i = 0; i < route.segment_count; i++) 790 for (size_t i = 0; i < route.segment_count; i++)
738 route.is_param[i] = (route.path_segments[i][0] == ':'); 791 route.is_param[i] = (route.path_segments[i][0] == ':');
739 792
740 Dowa_Array_Push(g_routes, route); 793 Dowa_Array_Push(g_routes, route);
757 return FALSE; 810 return FALSE;
758 } 811 }
759 812
760 for (size_t i = 0; i < route->segment_count; i++) 813 for (size_t i = 0; i < route->segment_count; i++)
761 { 814 {
762 // parameters 815 if (!route->is_param[i])
763 if (route->is_param[i]) 816 {
764 {
765 char *param_name = route->path_segments[i]; // e.g., ":id"
766 char *param_value = request_segments[i]; // e.g., "123"
767
768 // Should Copy to arena
769 char *key = Dowa_String_Copy_Arena(param_name, p_arena);
770 char *value = Dowa_String_Copy_Arena(param_value, p_arena);
771 Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
772 }
773 else
774 {
775 // Does not match.
776 if (strcmp(route->path_segments[i], request_segments[i]) != 0) 817 if (strcmp(route->path_segments[i], request_segments[i]) != 0)
777 { 818 {
778 Dowa_Arena_Free(p_temp_arena); 819 Dowa_Arena_Free(p_temp_arena);
779 return FALSE; 820 return FALSE;
780 } 821 }
781 } 822 }
782 } 823 }
783 824
825 for (size_t i = 0; i < route->segment_count; i++)
826 {
827 if (!route->is_param[i])
828 continue;
829 char *key = Dowa_String_Copy_Arena(route->path_segments[i], p_arena);
830 char *value = Dowa_String_Copy_Arena(request_segments[i], p_arena);
831 Dowa_HashMap_Push_Arena(*pp_request_map, key, value, p_arena);
832 }
833
784 Dowa_Arena_Free(p_temp_arena); 834 Dowa_Arena_Free(p_temp_arena);
785 return TRUE; 835 return TRUE;
786 } 836 }
787 837
788 Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method, 838 Seobeo_Route_Handler Seobeo_Router_Find_Handler(const char *method,
830 880
831 if (route->stream_handler && match_route_and_extract(route, path, pp_request_map, p_arena)) 881 if (route->stream_handler && match_route_and_extract(route, path, pp_request_map, p_arena))
832 return route->stream_handler; 882 return route->stream_handler;
833 } 883 }
834 884
885 return NULL;
886 }
887
888 Seobeo_SSE_Handler Seobeo_Router_Find_SSE_Handler(
889 const char *method,
890 const char *path,
891 Seobeo_Request_Entry **pp_request_map,
892 Dowa_Arena *p_arena)
893 {
894 if (g_routes == NULL || method == NULL || path == NULL)
895 return NULL;
896
897 size_t route_count = Dowa_Array_Length(g_routes);
898 for (size_t i = 0; i < route_count; i++)
899 {
900 Seobeo_Route *route = &g_routes[i];
901 if (strcmp(route->method, method) != 0)
902 continue;
903 if (route->sse_handler &&
904 match_route_and_extract(
905 route,
906 path,
907 pp_request_map,
908 p_arena))
909 {
910 return route->sse_handler;
911 }
912 }
835 return NULL; 913 return NULL;
836 } 914 }
837 915
838 void Seobeo_Router_Send_Response( 916 void Seobeo_Router_Send_Response(
839 Seobeo_Handle *p_handle, 917 Seobeo_Handle *p_handle,
956 Seobeo_Handle_Flush(p_handle); 1034 Seobeo_Handle_Flush(p_handle);
957 } 1035 }
958 1036
959 void Seobeo_Router_Destroy() 1037 void Seobeo_Router_Destroy()
960 { 1038 {
1039 Seobeo_SSE_Server_Destroy();
961 if (g_routes == NULL) 1040 if (g_routes == NULL)
962 return; 1041 return;
963 1042
964 size_t route_count = Dowa_Array_Length(g_routes); 1043 size_t route_count = Dowa_Array_Length(g_routes);
965 for (size_t i = 0; i < route_count; i++) 1044 for (size_t i = 0; i < route_count; i++)
966 { 1045 {
967 Seobeo_Route *route = &g_routes[i]; 1046 Seobeo_Route *route = &g_routes[i];
968 if (route->method) free(route->method); 1047 if (route->method) Dowa_Free(route->method);
969 if (route->path_pattern) free(route->path_pattern); 1048 if (route->path_pattern) Dowa_Free(route->path_pattern);
970 if (route->path_segments) Dowa_Array_Free(route->path_segments); 1049 if (route->path_segments) Dowa_Array_Free(route->path_segments);
971 if (route->is_param) free(route->is_param); 1050 if (route->is_param) Dowa_Free(route->is_param);
972 } 1051 }
973 Dowa_Array_Free(g_routes); 1052 Dowa_Array_Free(g_routes);
974 g_routes = NULL; 1053 g_routes = NULL;
975 } 1054 }
976 1055