37#include "../websocket/ws_async_internal.h"
40#define GQL_WS_CLOSE_BAD_REQUEST 4400
41#define GQL_WS_CLOSE_UNAUTHORIZED 4401
42#define GQL_WS_CLOSE_DUPLICATE_ID 4409
43#define GQL_WS_CLOSE_SUBSCRIBE_BEFORE_INIT 4429
49struct cwist_graphql_event_source {
77 if (!src || !
topic ||
topic[0] ==
'\0')
return false;
78 for (
size_t i = 0;
i < src->topic_count;
i++) {
81 if (src->topic_count == src->topic_cap) {
82 size_t new_cap = src->topic_cap ? src->topic_cap * 2 : 4;
84 if (!
nt)
return false;
85 if (src->topic_count > 0)
memcpy(
nt, src->topics, src->topic_count *
sizeof(
char *));
91 if (!copy)
return false;
92 src->topics[src->topic_count++] = copy;
103 for (
size_t i = 0;
i < src->topic_count;
i++)
cwist_free(src->topics[
i]);
115 for (
size_t i = 0;
i < src->topic_count;
i++) {
125typedef struct gql_sub_field {
129 struct gql_sub_field *next;
132struct cwist_graphql_ws {
145 if (!schema)
return NULL;
148 gws->schema = schema;
192 if (!gws || !field || field[0] ==
'\0' || !
subscribe_fn)
return false;
194 if (
strcmp(
f->name, field) == 0) {
201 if (!
f)
return false;
220typedef struct gql_ws_op {
224 struct gql_ws_conn *conn;
225 struct gql_ws_op *next;
228typedef struct gql_ws_conn {
238 size_t pending_count;
244typedef struct gql_ws_flush {
265 for (
size_t i = 0;
i < conn->pending_count;
i++)
cwist_free(conn->pending[
i]);
277 gql_ws_conn_free(conn);
311 if (
op->conn == conn) {
312 gql_ws_op_unlink(
op);
332static void gql_ws_send_json(cwist_websocket_async *ws,
cJSON *
msg) {
354static void gql_ws_send_error(cwist_websocket_async *ws,
const char *
id,
const char *message) {
358 if (!
err || !payload || !entry) {
370 gql_ws_send_json(ws,
err);
379 if (conn->pending_count == conn->pending_cap) {
380 size_t new_cap = conn->pending_cap ? conn->pending_cap * 2 : 8;
382 if (!
np)
return false;
383 if (conn->pending_count > 0)
384 memcpy(
np, conn->pending, conn->pending_count *
sizeof(
char *));
389 conn->pending[conn->pending_count++] =
msg;
400 if (!
msg || !
pl || !data || !value)
goto out;
419static void gql_ws_flush_cb(
void *ctx) {
429 if (!conn->dead && conn->ws && conn->pending_count > 0) {
430 count = conn->pending_count;
434 conn->pending_count = 0;
441 for (
size_t i = 0;
i <
count;
i++) {
450 gql_ws_conn_unref(conn);
459 flush->node.cb = gql_ws_flush_cb;
461 gql_ws_conn_ref(conn);
463 gql_ws_conn_unref(conn);
477 if (!gql_src_has_topic(
op->src,
topic))
continue;
478 char *
msg = gql_ws_build_next(
op, payload);
480 if (!gql_ws_pending_push(
op->conn,
msg)) {
487 if (conns[
i] ==
op->conn) {
503 gql_ws_conn_ref(
op->conn);
510 gql_ws_post_flush(conns[
i]);
511 gql_ws_conn_unref(conns[
i]);
521static void gql_ws_handle_subscribe(cwist_websocket_async *ws,
gql_ws_conn_t *conn,
524 if (!
cJSON_IsString(
id) || !id->valuestring || id->valuestring[0] ==
'\0') {
525 gql_ws_send_error(ws,
NULL,
"Subscribe message must carry a non-empty string 'id'");
528 const char *
op_id =
id->valuestring;
535 gql_ws_send_error(ws,
op_id,
"Subscribe payload must contain a string 'query'");
562 gql_ws_send_error(ws,
op_id,
"Expected a 'subscription' operation with one root field");
568 if (
strcmp(
f->name, field) == 0) {
576 snprintf(buf,
sizeof(buf),
"Cannot subscribe field '%s' on schema", field);
577 gql_ws_send_error(ws,
op_id, buf);
584 gql_ws_send_error(ws,
op_id,
"Subscription resolver rejected the operation");
591 gql_ws_send_error(ws,
op_id,
"Out of memory");
596 if (!
op->id || !
op->field) {
601 gql_ws_send_error(ws,
op_id,
"Out of memory");
608 op->next = g_broker_ops;
616 gql_ws_send_error(ws,
NULL,
"Message must carry a string 'type'");
619 const char *
t = type->valuestring;
621 if (!conn->init_done) {
622 if (
strcmp(
t,
"connection_init") == 0) {
623 conn->init_done =
true;
630 gql_ws_send_json(ws, ack);
631 }
else if (
strcmp(
t,
"subscribe") == 0) {
647 gql_ws_send_json(ws,
pong);
648 }
else if (
strcmp(
t,
"pong") == 0 ||
strcmp(
t,
"connection_init") == 0) {
650 }
else if (
strcmp(
t,
"subscribe") == 0) {
651 gql_ws_handle_subscribe(ws, conn,
msg);
652 }
else if (
strcmp(
t,
"complete") == 0) {
655 gql_ws_send_error(ws,
NULL,
"Complete message must carry a string 'id'");
660 if (
op->conn == conn &&
strcmp(
op->id, id->valuestring) == 0) {
661 gql_ws_op_unlink(
op);
679 if (!conn)
return NULL;
682 conn->reactor = reactor;
684 conn->init_done =
false;
686 conn->pending =
NULL;
687 conn->pending_count = 0;
688 conn->pending_cap = 0;
699 gql_ws_conn_purge_ops(conn);
700 for (
size_t i = 0;
i < conn->pending_count;
i++)
cwist_free(conn->pending[
i]);
701 conn->pending_count = 0;
703 gql_ws_conn_unref(conn);
707static void gql_ws_on_close(cwist_websocket_async *ws,
void *user_data) {
712static void gql_ws_on_message(cwist_websocket_async *ws,
cwist_ws_frame *
frame,
void *user_data) {
726 gql_ws_dispatch(ws, conn,
msg);
732 if (!gws || fd < 0 || !reactor)
return false;
746 gql_ws_conn_dispose(conn);
755 gql_ws_post_flush(conn);
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
void * cwist_alloc_array(size_t count, size_t elem_size)
Allocate zeroed memory for count elements.
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
void cwist_graphql_event_source_destroy(cwist_graphql_event_source_t *src)
Destroy an event source, freeing all topic strings and the set.
bool cwist_graphql_event_source_add_topic(cwist_graphql_event_source_t *src, const char *topic)
Add a topic to an event source.
bool cwist_graphql_ws_attach(cwist_graphql_ws_t *gws, int fd, cwist_reactor_t *reactor, const uint8_t *initial, size_t initial_len)
bool cwist_graphql_ws_add_subscription(cwist_graphql_ws_t *gws, const char *field, cwist_graphql_subscribe_fn subscribe_fn, void *ctx)
Register (or replace) the resolver for a subscription root field.
cwist_graphql_ws_t * cwist_graphql_ws_create(cwist_graphql_schema_t *schema)
Create a graphql-ws endpoint holding a subscription field registry.
#define GQL_WS_CLOSE_DUPLICATE_ID
#define GQL_WS_CLOSE_SUBSCRIBE_BEFORE_INIT
struct gql_ws_op gql_ws_op_t
struct gql_ws_flush gql_ws_flush_t
#define GQL_WS_CLOSE_BAD_REQUEST
#define GQL_WS_CLOSE_UNAUTHORIZED
size_t cwist_graphql_publish(const char *topic, const cJSON *payload)
cwist_graphql_event_source_t * cwist_graphql_event_source_create(void)
Create an empty event source (topic interest set).
void cwist_graphql_ws_destroy(cwist_graphql_ws_t *gws)
Destroy an endpoint and its subscription field registry.
struct gql_sub_field gql_sub_field_t
struct gql_ws_conn gql_ws_conn_t
struct cwist_graphql_schema cwist_graphql_schema_t
bool cwist_graphql_parse_subscription(const char *query, const cJSON *variables, char *field_out, size_t field_cap, cJSON **args_out)
Extract the root field name and parsed arguments of a subscription operation.
#define CWIST_GRAPHQL_MAX_QUERY_SIZE
GraphQL subscriptions over the graphql-ws WebSocket subprotocol (supported since v3....
cwist_graphql_event_source_t *(* cwist_graphql_subscribe_fn)(const cJSON *args, const cJSON *variables, void *ctx)
struct cwist_graphql_event_source cwist_graphql_event_source_t
struct cwist_graphql_ws cwist_graphql_ws_t
void cwist_websocket_frame_destroy(cwist_ws_frame *frame)
Destroy a frame.
struct cwist_reactor cwist_reactor_t
void cwist_websocket_async_close_code(cwist_websocket_async *ws, uint16_t code)
Initiate the WebSocket close handshake with a CLOSE frame carrying a status code.
cwist_websocket_async * cwist_websocket_async_attach_ex(int fd, cwist_reactor_t *reactor, cwist_ws_on_message_t on_message, cwist_ws_on_close_t on_close, void *user_data, const uint8_t *initial, size_t initial_len)
Attach an upgraded, HTTP-connection-owned fd to the reactor as an async WebSocket connection.
Reactor-driven, callback-shaped non-blocking WebSocket API (C1M mode).
int cwist_websocket_async_send(cwist_websocket_async *ws, cwist_ws_opcode_t opcode, const uint8_t *data, size_t len)
Serialize and send one FIN-terminated frame without blocking.
struct cwist_websocket_async cwist_websocket_async
void cwist_websocket_async_close(cwist_websocket_async *ws)
Initiate the close handshake without blocking.