Mercurial
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 |