cwist
Modern C web framework with libttak runtime
Loading...
Searching...
No Matches
graphql_ws.c
Go to the documentation of this file.
1
30#include <pthread.h>
31#include <stdatomic.h>
32#include <stdio.h>
33#include <stdlib.h>
34#include <string.h>
35#include <unistd.h>
36
37#include "../websocket/ws_async_internal.h"
38
39/* graphql-ws close codes (https://github.com/enisdenjo/graphql-ws/blob/master/PROTOCOL.md). */
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
44
45/* ------------------------------------------------------------------ */
46/* Event source */
47/* ------------------------------------------------------------------ */
48
49struct cwist_graphql_event_source {
50 char **topics;
51 size_t topic_count;
52 size_t topic_cap;
53};
54
63
77 if (!src || !topic || topic[0] == '\0') return false;
78 for (size_t i = 0; i < src->topic_count; i++) {
79 if (strcmp(src->topics[i], topic) == 0) return true; /* idempotent */
80 }
81 if (src->topic_count == src->topic_cap) {
82 size_t new_cap = src->topic_cap ? src->topic_cap * 2 : 4;
83 char **nt = (char **)cwist_alloc_array(new_cap, sizeof(char *));
84 if (!nt) return false;
85 if (src->topic_count > 0) memcpy(nt, src->topics, src->topic_count * sizeof(char *));
86 cwist_free(src->topics);
87 src->topics = nt;
88 src->topic_cap = new_cap;
89 }
90 char *copy = cwist_strdup(topic);
91 if (!copy) return false;
92 src->topics[src->topic_count++] = copy;
93 return true;
94}
95
102 if (!src) return;
103 for (size_t i = 0; i < src->topic_count; i++) cwist_free(src->topics[i]);
104 cwist_free(src->topics);
105 cwist_free(src);
106}
107
114static bool gql_src_has_topic(const cwist_graphql_event_source_t *src, const char *topic) {
115 for (size_t i = 0; i < src->topic_count; i++) {
116 if (strcmp(src->topics[i], topic) == 0) return true;
117 }
118 return false;
119}
120
121/* ------------------------------------------------------------------ */
122/* Endpoint (subscription field registry) */
123/* ------------------------------------------------------------------ */
124
125typedef struct gql_sub_field {
126 char *name;
128 void *ctx;
129 struct gql_sub_field *next;
131
132struct cwist_graphql_ws {
133 cwist_graphql_schema_t *schema; /* not owned */
134 gql_sub_field_t *subs;
135};
136
145 if (!schema) return NULL;
146 cwist_graphql_ws_t *gws = (cwist_graphql_ws_t *)cwist_alloc(sizeof(*gws));
147 if (gws) {
148 gws->schema = schema;
149 gws->subs = NULL;
150 }
151 return gws;
152}
153
163 if (!gws) return;
164 gql_sub_field_t *f = gws->subs;
165 while (f) {
166 gql_sub_field_t *next = f->next;
167 cwist_free(f->name);
168 cwist_free(f);
169 f = next;
170 }
171 cwist_free(gws);
172}
173
192 if (!gws || !field || field[0] == '\0' || !subscribe_fn) return false;
193 for (gql_sub_field_t *f = gws->subs; f; f = f->next) {
194 if (strcmp(f->name, field) == 0) {
195 f->fn = subscribe_fn;
196 f->ctx = ctx;
197 return true;
198 }
199 }
201 if (!f) return false;
202 f->name = cwist_strdup(field);
203 if (!f->name) {
204 cwist_free(f);
205 return false;
206 }
207 f->fn = subscribe_fn;
208 f->ctx = ctx;
209 f->next = gws->subs;
210 gws->subs = f;
211 return true;
212}
213
214/* ------------------------------------------------------------------ */
215/* Broker: operations, connections, pending queues */
216/* ------------------------------------------------------------------ */
217
218/* One live `subscribe` operation. Operations live in the global broker list
219 * while active; each belongs to exactly one connection. */
220typedef struct gql_ws_op {
221 char *id; /* client operation id (owned) */
222 char *field; /* subscription root field name (owned) */
223 cwist_graphql_event_source_t *src; /* topic interest (owned) */
224 struct gql_ws_conn *conn;
225 struct gql_ws_op *next;
227
228typedef struct gql_ws_conn {
229 cwist_graphql_ws_t *gws; /* not owned */
230 cwist_websocket_async *ws; /* NULL until attach succeeds; cleared at teardown */
231 cwist_reactor_t *reactor;
232 atomic_int refs;
233 bool init_done; /* connection_init received (reactor thread only) */
234 bool dead; /* teardown started; guarded by g_broker_lock */
235 /* Queued `next` messages (malloc'd strings via cJSON_Print). Guarded by
236 * g_broker_lock; drained by flush nodes on the reactor thread. */
237 char **pending;
238 size_t pending_count;
239 size_t pending_cap;
241
242/* Reactor-posted flush node: drains a connection's pending queue on the
243 * reactor thread. Holds a conn reference until dispatched. */
244typedef struct gql_ws_flush {
246 gql_ws_conn_t *conn;
248
249static pthread_mutex_t g_broker_lock = PTHREAD_MUTEX_INITIALIZER;
250static gql_ws_op_t *g_broker_ops; /* all active operations, all endpoints */
251
256static void gql_ws_conn_ref(gql_ws_conn_t *conn) {
258}
259
264static void gql_ws_conn_free(gql_ws_conn_t *conn) {
265 for (size_t i = 0; i < conn->pending_count; i++) cwist_free(conn->pending[i]);
266 cwist_free(conn->pending);
267 cwist_free(conn);
268}
269
275static void gql_ws_conn_unref(gql_ws_conn_t *conn) {
276 if (atomic_fetch_sub_explicit(&conn->refs, 1, memory_order_acq_rel) == 1)
277 gql_ws_conn_free(conn);
278}
279
283static void gql_ws_op_free(gql_ws_op_t *op) {
284 cwist_free(op->id);
285 cwist_free(op->field);
287 cwist_free(op);
288}
289
295static void gql_ws_op_unlink(gql_ws_op_t *victim) {
296 gql_ws_op_t **pp = &g_broker_ops;
297 while (*pp && *pp != victim) pp = &(*pp)->next;
298 if (*pp) *pp = victim->next;
299}
300
307static void gql_ws_conn_purge_ops(gql_ws_conn_t *conn) {
308 gql_ws_op_t *op = g_broker_ops;
309 while (op) {
310 gql_ws_op_t *next = op->next;
311 if (op->conn == conn) {
312 gql_ws_op_unlink(op);
313 gql_ws_op_free(op);
314 }
315 op = next;
316 }
317}
318
319/* ------------------------------------------------------------------ */
320/* Wire helpers (reactor thread only) */
321/* ------------------------------------------------------------------ */
322
332static void gql_ws_send_json(cwist_websocket_async *ws, cJSON *msg) {
335 if (!printed) {
337 return;
338 }
340 strlen(printed)) != 0)
343}
344
354static void gql_ws_send_error(cwist_websocket_async *ws, const char *id, const char *message) {
356 cJSON *payload = cJSON_CreateArray();
357 cJSON *entry = cJSON_CreateObject();
358 if (!err || !payload || !entry) {
360 cJSON_Delete(payload);
361 cJSON_Delete(entry);
363 return;
364 }
365 cJSON_AddStringToObject(err, "type", "error");
366 cJSON_AddStringToObject(err, "id", id ? id : "");
367 cJSON_AddStringToObject(entry, "message", message);
368 cJSON_AddItemToArray(payload, entry);
369 cJSON_AddItemToObject(err, "payload", payload);
370 gql_ws_send_json(ws, err);
371}
372
373/* ------------------------------------------------------------------ */
374/* Pending queue + flush delivery */
375/* ------------------------------------------------------------------ */
376
377/* Append a `next` message to the connection's queue (caller holds g_broker_lock). */
378static bool gql_ws_pending_push(gql_ws_conn_t *conn, char *msg) {
379 if (conn->pending_count == conn->pending_cap) {
380 size_t new_cap = conn->pending_cap ? conn->pending_cap * 2 : 8;
381 char **np = (char **)cwist_alloc_array(new_cap, sizeof(char *));
382 if (!np) return false;
383 if (conn->pending_count > 0)
384 memcpy(np, conn->pending, conn->pending_count * sizeof(char *));
385 cwist_free(conn->pending);
386 conn->pending = np;
387 conn->pending_cap = new_cap;
388 }
389 conn->pending[conn->pending_count++] = msg;
390 return true;
391}
392
393/* Serialize one `next` message for @p op (malloc'd string, or NULL on OOM). */
394static char *gql_ws_build_next(const gql_ws_op_t *op, const cJSON *payload) {
395 char *printed = NULL;
398 cJSON *data = cJSON_CreateObject();
399 cJSON *value = payload ? cJSON_Duplicate(payload, true) : cJSON_CreateNull();
400 if (!msg || !pl || !data || !value) goto out;
401 cJSON_AddStringToObject(msg, "type", "next");
402 cJSON_AddStringToObject(msg, "id", op->id);
403 cJSON_AddItemToObject(data, op->field, value);
404 value = NULL;
405 cJSON_AddItemToObject(pl, "data", data);
406 data = NULL;
407 cJSON_AddItemToObject(msg, "payload", pl);
408 pl = NULL;
410out:
411 cJSON_Delete(value);
412 cJSON_Delete(data);
415 return printed;
416}
417
418/* Runs on the reactor thread; drains the pending queue to the wire. */
419static void gql_ws_flush_cb(void *ctx) {
421 gql_ws_conn_t *conn = flush->conn;
422 char **msgs = NULL;
423 size_t count = 0;
424
425 pthread_mutex_lock(&g_broker_lock);
426 /* Teardown runs on this same thread, so conn->ws is stable here: once
427 * dead, the socket is gone and the queue was already freed. A NULL ws
428 * (attach still finalizing) leaves the queue for a later flush. */
429 if (!conn->dead && conn->ws && conn->pending_count > 0) {
430 count = conn->pending_count;
431 msgs = (char **)cwist_alloc_array(count, sizeof(char *));
432 if (msgs) {
433 memcpy(msgs, conn->pending, count * sizeof(char *));
434 conn->pending_count = 0;
435 } else {
436 count = 0; /* OOM: leave queued for the next flush */
437 }
438 }
439 pthread_mutex_unlock(&g_broker_lock);
440
441 for (size_t i = 0; i < count; i++) {
443 strlen(msgs[i])) != 0) {
445 }
446 cwist_free(msgs[i]);
447 }
449
450 gql_ws_conn_unref(conn);
452}
453
454/* Queue a flush node for @p conn; takes a reference released by the callback. */
455static void gql_ws_post_flush(gql_ws_conn_t *conn) {
457 if (!flush) return;
458 flush->conn = conn;
459 flush->node.cb = gql_ws_flush_cb;
460 flush->node.ctx = flush;
461 gql_ws_conn_ref(conn);
462 if (!cwist_reactor_post(conn->reactor, &flush->node)) {
463 gql_ws_conn_unref(conn);
465 }
466}
467
468size_t cwist_graphql_publish(const char *topic, const cJSON *payload) {
469 if (!topic || topic[0] == '\0') return 0;
470
471 gql_ws_conn_t **conns = NULL;
472 size_t conn_count = 0, conn_cap = 0;
473 size_t delivered = 0;
474
475 pthread_mutex_lock(&g_broker_lock);
476 for (gql_ws_op_t *op = g_broker_ops; op; op = op->next) {
477 if (!gql_src_has_topic(op->src, topic)) continue;
478 char *msg = gql_ws_build_next(op, payload);
479 if (!msg) continue;
480 if (!gql_ws_pending_push(op->conn, msg)) {
482 continue;
483 }
484 delivered++;
485 bool seen = false;
486 for (size_t i = 0; i < conn_count; i++) {
487 if (conns[i] == op->conn) {
488 seen = true;
489 break;
490 }
491 }
492 if (!seen) {
493 if (conn_count == conn_cap) {
494 size_t new_cap = conn_cap ? conn_cap * 2 : 4;
496 if (!nc) continue; /* queue stays; drained by an earlier flush */
497 if (conn_count > 0) memcpy(nc, conns, conn_count * sizeof(*nc));
498 cwist_free(conns);
499 conns = nc;
501 }
502 conns[conn_count] = op->conn;
503 gql_ws_conn_ref(op->conn);
504 conn_count++;
505 }
506 }
507 pthread_mutex_unlock(&g_broker_lock);
508
509 for (size_t i = 0; i < conn_count; i++) {
510 gql_ws_post_flush(conns[i]); /* adopts the reference */
511 gql_ws_conn_unref(conns[i]); /* release the publish-side reference */
512 }
513 cwist_free(conns);
514 return delivered;
515}
516
517/* ------------------------------------------------------------------ */
518/* Protocol handling (reactor thread only) */
519/* ------------------------------------------------------------------ */
520
521static void gql_ws_handle_subscribe(cwist_websocket_async *ws, gql_ws_conn_t *conn,
522 const cJSON *msg) {
523 const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id");
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'");
526 return;
527 }
528 const char *op_id = id->valuestring;
529
530 const cJSON *payload = cJSON_GetObjectItemCaseSensitive(msg, "payload");
531 const cJSON *query =
532 cJSON_IsObject(payload) ? cJSON_GetObjectItemCaseSensitive(payload, "query") : NULL;
533 if (!cJSON_IsString(query) || !query->valuestring ||
534 strlen(query->valuestring) > CWIST_GRAPHQL_MAX_QUERY_SIZE) {
535 gql_ws_send_error(ws, op_id, "Subscribe payload must contain a string 'query'");
536 return;
537 }
538
539 pthread_mutex_lock(&g_broker_lock);
540 bool duplicate = false;
541 for (gql_ws_op_t *op = g_broker_ops; op; op = op->next) {
542 if (op->conn == conn && strcmp(op->id, op_id) == 0) {
543 duplicate = true;
544 break;
545 }
546 }
547 pthread_mutex_unlock(&g_broker_lock);
548 if (duplicate) {
549 /* graphql-ws: a duplicate subscriber id is a connection-level
550 * violation, not an operation-level one. */
552 return;
553 }
554
555 const cJSON *variables =
556 cJSON_IsObject(payload) ? cJSON_GetObjectItemCaseSensitive(payload, "variables") : NULL;
557
558 char field[128];
559 cJSON *args = NULL;
560 if (!cwist_graphql_parse_subscription(query->valuestring, variables, field, sizeof(field),
561 &args)) {
562 gql_ws_send_error(ws, op_id, "Expected a 'subscription' operation with one root field");
563 return;
564 }
565
567 for (gql_sub_field_t *f = conn->gws->subs; f; f = f->next) {
568 if (strcmp(f->name, field) == 0) {
569 sf = f;
570 break;
571 }
572 }
573 if (!sf) {
575 char buf[192];
576 snprintf(buf, sizeof(buf), "Cannot subscribe field '%s' on schema", field);
577 gql_ws_send_error(ws, op_id, buf);
578 return;
579 }
580
583 if (!src) {
584 gql_ws_send_error(ws, op_id, "Subscription resolver rejected the operation");
585 return;
586 }
587
588 gql_ws_op_t *op = (gql_ws_op_t *)cwist_alloc(sizeof(*op));
589 if (!op) {
591 gql_ws_send_error(ws, op_id, "Out of memory");
592 return;
593 }
594 op->id = cwist_strdup(op_id);
595 op->field = cwist_strdup(field);
596 if (!op->id || !op->field) {
597 cwist_free(op->id);
598 cwist_free(op->field);
599 cwist_free(op);
601 gql_ws_send_error(ws, op_id, "Out of memory");
602 return;
603 }
604 op->src = src;
605 op->conn = conn;
606
607 pthread_mutex_lock(&g_broker_lock);
608 op->next = g_broker_ops;
609 g_broker_ops = op;
610 pthread_mutex_unlock(&g_broker_lock);
611}
612
613static void gql_ws_dispatch(cwist_websocket_async *ws, gql_ws_conn_t *conn, cJSON *msg) {
614 const cJSON *type = cJSON_GetObjectItemCaseSensitive(msg, "type");
615 if (!cJSON_IsString(type) || !type->valuestring) {
616 gql_ws_send_error(ws, NULL, "Message must carry a string 'type'");
617 return;
618 }
619 const char *t = type->valuestring;
620
621 if (!conn->init_done) {
622 if (strcmp(t, "connection_init") == 0) {
623 conn->init_done = true;
624 cJSON *ack = cJSON_CreateObject();
625 if (!ack) {
627 return;
628 }
629 cJSON_AddStringToObject(ack, "type", "connection_ack");
630 gql_ws_send_json(ws, ack);
631 } else if (strcmp(t, "subscribe") == 0) {
633 } else {
635 }
636 return;
637 }
638 if (strcmp(t, "ping") == 0) {
640 if (!pong) {
642 return;
643 }
644 cJSON_AddStringToObject(pong, "type", "pong");
645 const cJSON *pl = cJSON_GetObjectItemCaseSensitive(msg, "payload");
646 if (pl) cJSON_AddItemToObject(pong, "payload", cJSON_Duplicate(pl, true));
647 gql_ws_send_json(ws, pong);
648 } else if (strcmp(t, "pong") == 0 || strcmp(t, "connection_init") == 0) {
649 /* Keepalive / duplicate init: nothing to do. */
650 } else if (strcmp(t, "subscribe") == 0) {
651 gql_ws_handle_subscribe(ws, conn, msg);
652 } else if (strcmp(t, "complete") == 0) {
653 const cJSON *id = cJSON_GetObjectItemCaseSensitive(msg, "id");
654 if (!cJSON_IsString(id) || !id->valuestring) {
655 gql_ws_send_error(ws, NULL, "Complete message must carry a string 'id'");
656 return;
657 }
658 pthread_mutex_lock(&g_broker_lock);
659 for (gql_ws_op_t *op = g_broker_ops; op; op = op->next) {
660 if (op->conn == conn && strcmp(op->id, id->valuestring) == 0) {
661 gql_ws_op_unlink(op);
662 gql_ws_op_free(op);
663 break;
664 }
665 }
666 pthread_mutex_unlock(&g_broker_lock);
667 /* A client `complete` gets no response; the operation is simply gone. */
668 } else {
670 }
671}
672
673/* ------------------------------------------------------------------ */
674/* Connection lifecycle */
675/* ------------------------------------------------------------------ */
676
677static gql_ws_conn_t *gql_ws_conn_create(cwist_graphql_ws_t *gws, cwist_reactor_t *reactor) {
678 gql_ws_conn_t *conn = (gql_ws_conn_t *)cwist_alloc(sizeof(*conn));
679 if (!conn) return NULL;
680 conn->gws = gws;
681 conn->ws = NULL;
682 conn->reactor = reactor;
683 atomic_init(&conn->refs, 1); /* owned by the protocol layer until teardown */
684 conn->init_done = false;
685 conn->dead = false;
686 conn->pending = NULL;
687 conn->pending_count = 0;
688 conn->pending_cap = 0;
689 return conn;
690}
691
692/* Shared by on_close (teardown) and the attach-failure path: mark dead, purge
693 * broker operations and the pending queue, release the protocol reference.
694 * Flush nodes still in flight keep the state alive and no-op on dispatch. */
695static void gql_ws_conn_dispose(gql_ws_conn_t *conn) {
696 pthread_mutex_lock(&g_broker_lock);
697 conn->dead = true;
698 conn->ws = NULL;
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;
702 pthread_mutex_unlock(&g_broker_lock);
703 gql_ws_conn_unref(conn);
704}
705
706/* WS layer teardown hook (reactor thread). */
707static void gql_ws_on_close(cwist_websocket_async *ws, void *user_data) {
708 (void)ws;
709 gql_ws_conn_dispose((gql_ws_conn_t *)user_data);
710}
711
712static void gql_ws_on_message(cwist_websocket_async *ws, cwist_ws_frame *frame, void *user_data) {
713 gql_ws_conn_t *conn = (gql_ws_conn_t *)user_data;
714 /* CLOSE is handled by the WS layer itself (echo + teardown); PING already
715 * got its PONG. Binary frames carry no graphql-ws semantics. */
716 if (frame->opcode != CWIST_WS_FRAME_TEXT) {
718 return;
719 }
720 cJSON *msg = cJSON_Parse((const char *)frame->payload);
722 if (!msg) {
724 return;
725 }
726 gql_ws_dispatch(ws, conn, msg);
728}
729
731 const uint8_t *initial, size_t initial_len) {
732 if (!gws || fd < 0 || !reactor) return false;
733
734 gql_ws_conn_t *conn = gql_ws_conn_create(gws, reactor);
735 if (!conn) {
736 close(fd);
737 return false;
738 }
739
740 /* on_close releases the protocol state at teardown. */
742 fd, reactor, gql_ws_on_message, gql_ws_on_close, conn, initial, initial_len);
743 if (!ws) {
744 /* Attach failed after any inline initial-bytes delivery; no on_close
745 * will ever run, so clean up here. */
746 gql_ws_conn_dispose(conn);
747 return false;
748 }
749
750 pthread_mutex_lock(&g_broker_lock);
751 conn->ws = ws;
752 pthread_mutex_unlock(&g_broker_lock);
753
754 /* Drain anything queued by inline initial-bytes delivery. */
755 gql_ws_post_flush(conn);
756 return true;
757}
Memory allocation wrappers backed by libttak.
void cwist_free(void *ptr)
Free memory obtained via cwist_alloc/cwist_strdup/etc.
Definition alloc.c:52
char * cwist_strdup(const char *src)
Duplicate a string using libttak-backed storage.
Definition alloc.c:32
void * cwist_alloc_array(size_t count, size_t elem_size)
Allocate zeroed memory for count elements.
Definition alloc.c:21
void * cwist_alloc(size_t size)
Allocate zeroed memory tracked by libttak.
Definition alloc.c:17
void cwist_graphql_event_source_destroy(cwist_graphql_event_source_t *src)
Destroy an event source, freeing all topic strings and the set.
Definition graphql_ws.c:101
bool cwist_graphql_event_source_add_topic(cwist_graphql_event_source_t *src, const char *topic)
Add a topic to an event source.
Definition graphql_ws.c:76
bool cwist_graphql_ws_attach(cwist_graphql_ws_t *gws, int fd, cwist_reactor_t *reactor, const uint8_t *initial, size_t initial_len)
Definition graphql_ws.c:730
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.
Definition graphql_ws.c:190
cwist_graphql_ws_t * cwist_graphql_ws_create(cwist_graphql_schema_t *schema)
Create a graphql-ws endpoint holding a subscription field registry.
Definition graphql_ws.c:144
#define GQL_WS_CLOSE_DUPLICATE_ID
Definition graphql_ws.c:42
#define GQL_WS_CLOSE_SUBSCRIBE_BEFORE_INIT
Definition graphql_ws.c:43
struct gql_ws_op gql_ws_op_t
struct gql_ws_flush gql_ws_flush_t
#define GQL_WS_CLOSE_BAD_REQUEST
Definition graphql_ws.c:40
#define GQL_WS_CLOSE_UNAUTHORIZED
Definition graphql_ws.c:41
size_t cwist_graphql_publish(const char *topic, const cJSON *payload)
Definition graphql_ws.c:468
cwist_graphql_event_source_t * cwist_graphql_event_source_create(void)
Create an empty event source (topic interest set).
Definition graphql_ws.c:60
void cwist_graphql_ws_destroy(cwist_graphql_ws_t *gws)
Destroy an endpoint and its subscription field registry.
Definition graphql_ws.c:162
struct gql_sub_field gql_sub_field_t
struct gql_ws_conn gql_ws_conn_t
struct cwist_graphql_schema cwist_graphql_schema_t
Definition graphql.h:14
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.
Definition graphql.c:557
#define CWIST_GRAPHQL_MAX_QUERY_SIZE
Definition graphql.h:15
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)
Definition graphql_ws.h:46
struct cwist_graphql_event_source cwist_graphql_event_source_t
Definition graphql_ws.h:34
struct cwist_graphql_ws cwist_graphql_ws_t
Definition graphql_ws.h:33
@ CWIST_WS_FRAME_TEXT
Definition websocket.h:15
void cwist_websocket_frame_destroy(cwist_ws_frame *frame)
Destroy a frame.
Definition websocket.c:436
size_t count
Definition otel.c:58
size_t cap
Definition otel.c:56
struct cwist_reactor cwist_reactor_t
Definition reactor.h:14
Definition reactor.h:46
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.