iodine 0.7.59 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (160) hide show
  1. checksums.yaml +4 -4
  2. data/.clang-format +15 -0
  3. data/.rspec +1 -0
  4. data/.yardopts +1 -3
  5. data/CHANGELOG.md +132 -12
  6. data/Gemfile +5 -7
  7. data/LICENSE.txt +1 -1
  8. data/README.md +147 -671
  9. data/Rakefile +9 -16
  10. data/examples/chatroom.nru +9 -0
  11. data/examples/client +74 -0
  12. data/examples/config-hello.nru +9 -0
  13. data/examples/config-hello.ru +11 -0
  14. data/examples/config-hijack.ru +13 -0
  15. data/examples/config-shootout.ru +43 -0
  16. data/examples/config.nru +120 -0
  17. data/examples/config.rb +37 -0
  18. data/examples/config.ru +42 -53
  19. data/examples/fake +24 -0
  20. data/examples/hello_neorack +137 -0
  21. data/examples/routes.nru +29 -0
  22. data/examples/routes.ru +42 -0
  23. data/exe/iodine +218 -258
  24. data/ext/iodine/extconf.rb +27 -100
  25. data/ext/iodine/fio-stl.h +128336 -0
  26. data/ext/iodine/fio-stl.md +26396 -0
  27. data/ext/iodine/iodine.c +213 -1393
  28. data/ext/iodine/iodine.h +250 -55
  29. data/ext/iodine/iodine_arg_helper.h +329 -0
  30. data/ext/iodine/iodine_caller.h +234 -25
  31. data/ext/iodine/iodine_cli.h +434 -0
  32. data/ext/iodine/iodine_compression.h +312 -0
  33. data/ext/iodine/iodine_connection.h +3484 -34
  34. data/ext/iodine/iodine_core.h +363 -0
  35. data/ext/iodine/iodine_crypto.h +1441 -0
  36. data/ext/iodine/iodine_defer.h +225 -4
  37. data/ext/iodine/iodine_json.h +738 -4
  38. data/ext/iodine/iodine_listener.h +263 -0
  39. data/ext/iodine/iodine_log.h +297 -0
  40. data/ext/iodine/iodine_minimap.h +543 -0
  41. data/ext/iodine/iodine_mustache.h +461 -4
  42. data/ext/iodine/iodine_pubsub_eng.h +729 -0
  43. data/ext/iodine/iodine_pubsub_history.h +547 -0
  44. data/ext/iodine/iodine_pubsub_msg.h +228 -0
  45. data/ext/iodine/iodine_redis.h +322 -0
  46. data/ext/iodine/iodine_store.h +418 -16
  47. data/ext/iodine/iodine_threads.h +279 -0
  48. data/ext/iodine/iodine_tls.h +515 -6
  49. data/ext/iodine/iodine_utils.h +923 -0
  50. data/iodine.gemspec +37 -42
  51. data/lib/iodine/benchmark.rb +1690 -0
  52. data/lib/iodine/documentation.rb +3085 -0
  53. data/lib/iodine/version.rb +17 -1
  54. data/lib/iodine.rb +54 -255
  55. data/logo.svg +36 -0
  56. data/mustache.md +55 -0
  57. data/sig/iodine.rbs +4 -0
  58. metadata +58 -184
  59. data/.github/ISSUE_TEMPLATE/bug_report.md +0 -40
  60. data/.github/workflows/ruby.yml +0 -42
  61. data/.gitignore +0 -20
  62. data/LIMITS.md +0 -41
  63. data/SPEC-PubSub-Draft.md +0 -159
  64. data/SPEC-WebSocket-Draft.md +0 -239
  65. data/bin/console +0 -22
  66. data/bin/info.md +0 -353
  67. data/bin/mustache_bench.rb +0 -100
  68. data/bin/poc/Gemfile.lock +0 -23
  69. data/bin/poc/README.md +0 -37
  70. data/bin/poc/config.ru +0 -66
  71. data/bin/poc/gemfile +0 -1
  72. data/bin/poc/www/index.html +0 -57
  73. data/examples/async_task.ru +0 -92
  74. data/examples/bates/README.md +0 -3
  75. data/examples/bates/config.ru +0 -342
  76. data/examples/bates/david+bold.pdf +0 -0
  77. data/examples/bates/public/drop-pdf.png +0 -0
  78. data/examples/bates/public/index.html +0 -600
  79. data/examples/echo.ru +0 -59
  80. data/examples/etag.ru +0 -16
  81. data/examples/hello.ru +0 -29
  82. data/examples/pubsub_engine.ru +0 -81
  83. data/examples/rack3.ru +0 -12
  84. data/examples/redis.ru +0 -70
  85. data/examples/shootout.ru +0 -73
  86. data/examples/sub-protocols.ru +0 -90
  87. data/examples/tcp_client.rb +0 -66
  88. data/examples/x-sendfile.ru +0 -14
  89. data/ext/iodine/fio.c +0 -11995
  90. data/ext/iodine/fio.h +0 -6371
  91. data/ext/iodine/fio_cli.c +0 -431
  92. data/ext/iodine/fio_cli.h +0 -189
  93. data/ext/iodine/fio_json_parser.h +0 -688
  94. data/ext/iodine/fio_siphash.c +0 -157
  95. data/ext/iodine/fio_siphash.h +0 -37
  96. data/ext/iodine/fio_tls.h +0 -129
  97. data/ext/iodine/fio_tls_missing.c +0 -649
  98. data/ext/iodine/fio_tls_openssl.c +0 -1106
  99. data/ext/iodine/fio_tmpfile.h +0 -50
  100. data/ext/iodine/fiobj.h +0 -44
  101. data/ext/iodine/fiobj4fio.h +0 -21
  102. data/ext/iodine/fiobj_ary.c +0 -333
  103. data/ext/iodine/fiobj_ary.h +0 -139
  104. data/ext/iodine/fiobj_data.c +0 -1187
  105. data/ext/iodine/fiobj_data.h +0 -167
  106. data/ext/iodine/fiobj_hash.c +0 -409
  107. data/ext/iodine/fiobj_hash.h +0 -176
  108. data/ext/iodine/fiobj_json.c +0 -622
  109. data/ext/iodine/fiobj_json.h +0 -68
  110. data/ext/iodine/fiobj_mem.h +0 -71
  111. data/ext/iodine/fiobj_mustache.c +0 -317
  112. data/ext/iodine/fiobj_mustache.h +0 -62
  113. data/ext/iodine/fiobj_numbers.c +0 -344
  114. data/ext/iodine/fiobj_numbers.h +0 -127
  115. data/ext/iodine/fiobj_str.c +0 -433
  116. data/ext/iodine/fiobj_str.h +0 -172
  117. data/ext/iodine/fiobject.c +0 -620
  118. data/ext/iodine/fiobject.h +0 -655
  119. data/ext/iodine/hpack.h +0 -1923
  120. data/ext/iodine/http.c +0 -2743
  121. data/ext/iodine/http.h +0 -1002
  122. data/ext/iodine/http1.c +0 -910
  123. data/ext/iodine/http1.h +0 -29
  124. data/ext/iodine/http1_parser.h +0 -878
  125. data/ext/iodine/http_internal.c +0 -1279
  126. data/ext/iodine/http_internal.h +0 -237
  127. data/ext/iodine/http_mime_parser.h +0 -350
  128. data/ext/iodine/iodine_caller.c +0 -230
  129. data/ext/iodine/iodine_connection.c +0 -941
  130. data/ext/iodine/iodine_defer.c +0 -420
  131. data/ext/iodine/iodine_fiobj2rb.h +0 -120
  132. data/ext/iodine/iodine_helpers.c +0 -282
  133. data/ext/iodine/iodine_helpers.h +0 -12
  134. data/ext/iodine/iodine_http.c +0 -1181
  135. data/ext/iodine/iodine_http.h +0 -23
  136. data/ext/iodine/iodine_json.c +0 -302
  137. data/ext/iodine/iodine_mustache.c +0 -567
  138. data/ext/iodine/iodine_pubsub.c +0 -580
  139. data/ext/iodine/iodine_pubsub.h +0 -26
  140. data/ext/iodine/iodine_rack_io.c +0 -273
  141. data/ext/iodine/iodine_rack_io.h +0 -20
  142. data/ext/iodine/iodine_store.c +0 -146
  143. data/ext/iodine/iodine_tcp.c +0 -346
  144. data/ext/iodine/iodine_tcp.h +0 -13
  145. data/ext/iodine/iodine_tls.c +0 -261
  146. data/ext/iodine/mustache_parser.h +0 -1546
  147. data/ext/iodine/redis_engine.c +0 -957
  148. data/ext/iodine/redis_engine.h +0 -79
  149. data/ext/iodine/resp_parser.h +0 -317
  150. data/ext/iodine/websocket_parser.h +0 -506
  151. data/ext/iodine/websockets.c +0 -743
  152. data/ext/iodine/websockets.h +0 -185
  153. data/lib/iodine/connection.rb +0 -61
  154. data/lib/iodine/json.rb +0 -42
  155. data/lib/iodine/mustache.rb +0 -113
  156. data/lib/iodine/pubsub.rb +0 -55
  157. data/lib/iodine/rack_utils.rb +0 -43
  158. data/lib/iodine/tls.rb +0 -16
  159. data/lib/rack/handler/iodine.rb +0 -33
  160. data/logo.png +0 -0
@@ -0,0 +1,228 @@
1
+ #ifndef H___IODINE_PUBSUB_MSG___H
2
+ #define H___IODINE_PUBSUB_MSG___H
3
+ #include "iodine.h"
4
+
5
+ /* *****************************************************************************
6
+ Iodine PubSub Message - Published Message Wrapper
7
+
8
+ This module provides the Iodine::PubSub::Message Ruby class which represents
9
+ a message received through the pub/sub system. Message objects are passed
10
+ to subscription callbacks and custom engine publish handlers.
11
+
12
+ Message Properties (readable and writable):
13
+ - id - Unique message identifier (Integer)
14
+ - channel - Channel name the message was published to (String)
15
+ - event - Alias for channel
16
+ - filter - Filter value (Integer, reserved)
17
+ - message - The message payload (String)
18
+ - msg - Alias for message
19
+ - data - Alias for message
20
+ - published - Timestamp when message was published (Integer)
21
+ - to_s - Returns the message payload (String)
22
+
23
+ Ruby API (Iodine::PubSub::Message):
24
+ - message.id - Get message ID
25
+ - message.channel - Get channel name
26
+ - message.message - Get message payload
27
+ - message.published - Get publish timestamp
28
+ - message.id = val - Set message ID (for custom engines)
29
+ - etc.
30
+ ***************************************************************************** */
31
+
32
+ /* *****************************************************************************
33
+ Ruby PubSub Message Object - Internal Types
34
+ ***************************************************************************** */
35
+
36
+ /**
37
+ * Enum for indexing into the message's VALUE store array.
38
+ * Each property is stored at a specific index for fast access.
39
+ */
40
+ typedef enum {
41
+ IODINE_PUBSUB_MSG_STORE_id, /**< Message unique ID */
42
+ IODINE_PUBSUB_MSG_STORE_channel, /**< Channel name */
43
+ IODINE_PUBSUB_MSG_STORE_filter, /**< Filter value */
44
+ IODINE_PUBSUB_MSG_STORE_message, /**< Message payload */
45
+ IODINE_PUBSUB_MSG_STORE_published, /**< Publish timestamp */
46
+ IODINE_PUBSUB_MSG_STORE_FINISH, /**< Sentinel - array size */
47
+ } iodine_pubsub_msg_store_e;
48
+
49
+ /**
50
+ * Internal structure representing a PubSub message.
51
+ *
52
+ * Stores Ruby VALUE objects for each message property in an array
53
+ * indexed by iodine_pubsub_msg_store_e values.
54
+ */
55
+ typedef struct iodine_pubsub_msg_wrapper_s {
56
+ const fio_pubsub_msg_s *msg; /**< Original C message (may be NULL) */
57
+ VALUE
58
+ store[IODINE_PUBSUB_MSG_STORE_FINISH]; /**< Ruby values for properties */
59
+ } iodine_pubsub_msg_wrapper_s;
60
+
61
+ static size_t iodine_pubsub_msg_data_size(const void *ptr_) {
62
+ iodine_pubsub_msg_wrapper_s *m = (iodine_pubsub_msg_wrapper_s *)ptr_;
63
+ return sizeof(*m) + (m->msg ? (sizeof(m->msg[0]) + m->msg->message.len +
64
+ m->msg->channel.len)
65
+ : 0);
66
+ }
67
+
68
+ static void iodine_pubsub_msg_mark(void *m_) {
69
+ iodine_pubsub_msg_wrapper_s *m = (iodine_pubsub_msg_wrapper_s *)m_;
70
+ for (size_t i = 0; i < IODINE_PUBSUB_MSG_STORE_FINISH; ++i)
71
+ if (!IODINE_STORE_IS_SKIP(m->store[i]))
72
+ rb_gc_mark(m->store[i]);
73
+ }
74
+
75
+ static void iodine_pubsub_msg_free(void *ptr_) {
76
+ iodine_pubsub_msg_wrapper_s *c = (iodine_pubsub_msg_wrapper_s *)ptr_;
77
+ ruby_xfree(c);
78
+ FIO_LEAK_COUNTER_ON_FREE(iodine_pubsub_msg);
79
+ }
80
+
81
+ static const rb_data_type_t IODINE_PUBSUB_MSG_DATA_TYPE = {
82
+ .wrap_struct_name = "IodinePSMessage",
83
+ .function =
84
+ {
85
+ .dmark = iodine_pubsub_msg_mark,
86
+ .dfree = iodine_pubsub_msg_free,
87
+ .dsize = iodine_pubsub_msg_data_size,
88
+ },
89
+ .data = NULL,
90
+ // .flags = RUBY_TYPED_FREE_IMMEDIATELY,
91
+ };
92
+
93
+ static VALUE iodine_pubsub_msg_alloc(VALUE klass) {
94
+ iodine_pubsub_msg_wrapper_s *m =
95
+ (iodine_pubsub_msg_wrapper_s *)ruby_xmalloc(sizeof(*m));
96
+ if (!m)
97
+ goto no_memory;
98
+ *m = (iodine_pubsub_msg_wrapper_s){0};
99
+ for (size_t i = 0; i < IODINE_PUBSUB_MSG_STORE_FINISH; ++i)
100
+ m->store[i] = Qnil;
101
+ FIO_LEAK_COUNTER_ON_ALLOC(iodine_pubsub_msg);
102
+ return TypedData_Wrap_Struct(klass, &IODINE_PUBSUB_MSG_DATA_TYPE, m);
103
+ no_memory:
104
+ FIO_LOG_FATAL("Memory allocation failed");
105
+ fio_io_stop();
106
+ return Qnil;
107
+ }
108
+
109
+ static iodine_pubsub_msg_wrapper_s *iodine_pubsub_msg_get(VALUE self) {
110
+ iodine_pubsub_msg_wrapper_s *m;
111
+ TypedData_Get_Struct(self,
112
+ iodine_pubsub_msg_wrapper_s,
113
+ &IODINE_PUBSUB_MSG_DATA_TYPE,
114
+ m);
115
+ return m;
116
+ }
117
+
118
+ /**
119
+ * Creates a new Iodine::PubSub::Message Ruby object from a C message.
120
+ *
121
+ * Copies all message properties from the C struct into Ruby VALUE objects.
122
+ * The returned message is held in the STORE to prevent GC.
123
+ *
124
+ * @param msg The C message struct to wrap
125
+ * @return New Iodine::PubSub::Message VALUE
126
+ */
127
+ static VALUE iodine_pubsub_msg_new(const fio_pubsub_msg_s *msg) {
128
+ VALUE m = rb_obj_alloc(iodine_rb_IODINE_PUBSUB_MSG);
129
+ STORE.hold(m);
130
+ iodine_pubsub_msg_wrapper_s *c = iodine_pubsub_msg_get(m);
131
+ c->store[IODINE_PUBSUB_MSG_STORE_id] = ULL2NUM(msg->id);
132
+ c->store[IODINE_PUBSUB_MSG_STORE_channel] =
133
+ (msg->channel.len ? rb_enc_str_new(msg->channel.buf,
134
+ msg->channel.len,
135
+ IodineBinaryEncoding)
136
+ : Qnil);
137
+ ;
138
+ c->store[IODINE_PUBSUB_MSG_STORE_filter] =
139
+ (msg->filter ? INT2NUM(((int16_t)(msg->filter))) : Qnil);
140
+ c->store[IODINE_PUBSUB_MSG_STORE_message] =
141
+ (msg->message.len ? rb_enc_str_new(msg->message.buf,
142
+ msg->message.len,
143
+ IodineBinaryEncoding)
144
+ : Qnil);
145
+ c->store[IODINE_PUBSUB_MSG_STORE_published] =
146
+ (msg->timestamp ? ULL2NUM(msg->timestamp) : Qnil);
147
+ return m;
148
+ }
149
+
150
+ /**
151
+ * Macro to define getter and setter functions for message properties.
152
+ *
153
+ * Generates:
154
+ * - iodine_pubsub_msg_<name>_get(self) - Returns the property value
155
+ * - iodine_pubsub_msg_<name>_set(self, val) - Sets the property value
156
+ *
157
+ * @param val_name The property name (id, channel, filter, message, published)
158
+ */
159
+ #define IODINE_DEF_GET_SET_FUNC(val_name, ...) \
160
+ /** Returns the message's val_name */ \
161
+ static VALUE iodine_pubsub_msg_##val_name##_get(VALUE self) { \
162
+ iodine_pubsub_msg_wrapper_s *c = iodine_pubsub_msg_get(self); \
163
+ if (!c) \
164
+ return Qnil; \
165
+ return c->store[IODINE_PUBSUB_MSG_STORE_##val_name]; \
166
+ } \
167
+ /** Sets the message's val_name */ \
168
+ static VALUE iodine_pubsub_msg_##val_name##_set(VALUE self, VALUE val) { \
169
+ iodine_pubsub_msg_wrapper_s *c = iodine_pubsub_msg_get(self); \
170
+ if (!c) \
171
+ return Qnil; \
172
+ return (c->store[IODINE_PUBSUB_MSG_STORE_##val_name] = val); \
173
+ }
174
+
175
+ /* Generate getter/setter functions for all message properties */
176
+ IODINE_DEF_GET_SET_FUNC(id); /* message.id / message.id= */
177
+ IODINE_DEF_GET_SET_FUNC(channel); /* message.channel / message.channel= */
178
+ IODINE_DEF_GET_SET_FUNC(filter); /* message.filter / message.filter= */
179
+ IODINE_DEF_GET_SET_FUNC(message); /* message.message / message.message= */
180
+ IODINE_DEF_GET_SET_FUNC(published); /* message.published / message.published= */
181
+
182
+ #undef IODINE_DEF_GET_SET_FUNC
183
+
184
+ /* *****************************************************************************
185
+ Initialize - Ruby Class Registration
186
+ ***************************************************************************** */
187
+
188
+ /**
189
+ * Initializes the Iodine::PubSub::Message Ruby class.
190
+ *
191
+ * Defines the Message class under Iodine::PubSub with:
192
+ * - Getter methods: id, channel, event, filter, message, msg, data, published,
193
+ * to_s
194
+ * - Setter methods: id=, channel=, event=, filter=, message=, msg=, data=,
195
+ * published=
196
+ *
197
+ * Note: event/msg/data are aliases for channel/message respectively.
198
+ */
199
+ static void Init_Iodine_PubSub_Message(void) {
200
+ // clang-format off
201
+ iodine_rb_IODINE_PUBSUB_MSG = rb_define_class_under(iodine_rb_IODINE_PUBSUB, "Message", rb_cObject);
202
+ STORE.hold(iodine_rb_IODINE_PUBSUB_MSG);
203
+ rb_define_alloc_func(iodine_rb_IODINE_PUBSUB_MSG, iodine_pubsub_msg_alloc);
204
+
205
+ /* Getter methods */
206
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "id", iodine_pubsub_msg_id_get, 0);
207
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "channel", iodine_pubsub_msg_channel_get, 0);
208
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "event", iodine_pubsub_msg_channel_get, 0);
209
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "filter", iodine_pubsub_msg_filter_get, 0);
210
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "message", iodine_pubsub_msg_message_get, 0);
211
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "msg", iodine_pubsub_msg_message_get, 0);
212
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "data", iodine_pubsub_msg_message_get, 0);
213
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "published", iodine_pubsub_msg_published_get, 0);
214
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "to_s", iodine_pubsub_msg_message_get, 0);
215
+
216
+ /* Setter methods */
217
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "id=", iodine_pubsub_msg_id_set, 1);
218
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "channel=", iodine_pubsub_msg_channel_set, 1);
219
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "event=", iodine_pubsub_msg_channel_set, 1);
220
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "filter=", iodine_pubsub_msg_filter_set, 1);
221
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "message=", iodine_pubsub_msg_message_set, 1);
222
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "msg=", iodine_pubsub_msg_message_set, 1);
223
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "data=", iodine_pubsub_msg_message_set, 1);
224
+ rb_define_method(iodine_rb_IODINE_PUBSUB_MSG, "published=", iodine_pubsub_msg_published_set, 1);
225
+ // clang-format on
226
+ }
227
+
228
+ #endif /* H___IODINE_PUBSUB_MSG___H */
@@ -0,0 +1,322 @@
1
+ #ifndef H___IODINE_REDIS___H
2
+ #define H___IODINE_REDIS___H
3
+
4
+ /* *****************************************************************************
5
+ RESP3 PubSub Engine Wrapper for Ruby
6
+
7
+ Exposes the facil.io Redis engine as Iodine::PubSub::Engine::RESP3.
8
+
9
+ Usage:
10
+ resp3 = Iodine::PubSub::Engine::RESP3.new("redis://localhost:6379/", ping: 50)
11
+ Iodine::PubSub.default = resp3
12
+ resp3.cmd("SET", "key", "value") { |result| puts result }
13
+
14
+ ***************************************************************************** */
15
+
16
+ static VALUE iodine_rb_IODINE_REDIS;
17
+
18
+ /* *****************************************************************************
19
+ Redis Command Callback Context
20
+ ***************************************************************************** */
21
+
22
+ typedef struct iodine_redis_cmd_ctx_s {
23
+ VALUE block;
24
+ fio_pubsub_engine_s *engine;
25
+ } iodine_redis_cmd_ctx_s;
26
+
27
+ /* *****************************************************************************
28
+ Redis Command Callback - Called from IO thread
29
+ ***************************************************************************** */
30
+
31
+ typedef struct iodine_redis_callback_args_s {
32
+ VALUE block;
33
+ FIOBJ reply;
34
+ } iodine_redis_callback_args_s;
35
+
36
+ static void *iodine_redis_callback_in_gvl(void *arg_) {
37
+ iodine_redis_callback_args_s *args = (iodine_redis_callback_args_s *)arg_;
38
+ VALUE result = iodine_fiobj2ruby(args->reply);
39
+ STORE.hold(result);
40
+ iodine_ruby_call_inside(args->block, IODINE_CALL_ID, 1, &result);
41
+ STORE.release(result);
42
+ return NULL;
43
+ }
44
+
45
+ static void iodine_redis_cmd_callback(fio_pubsub_engine_s *e,
46
+ FIOBJ reply,
47
+ void *udata) {
48
+ iodine_redis_cmd_ctx_s *ctx = (iodine_redis_cmd_ctx_s *)udata;
49
+ if (ctx->block != Qnil) {
50
+ iodine_redis_callback_args_s args = {
51
+ .block = ctx->block,
52
+ .reply = reply,
53
+ };
54
+ rb_thread_call_with_gvl(iodine_redis_callback_in_gvl, &args);
55
+ STORE.release(ctx->block);
56
+ }
57
+ FIO_MEM_FREE(ctx, sizeof(*ctx));
58
+ (void)e;
59
+ }
60
+
61
+ /* *****************************************************************************
62
+ Ruby Methods
63
+ ***************************************************************************** */
64
+
65
+ /**
66
+ * @!method initialize(url, ping: 0)
67
+ * Creates a new RESP3 Pub/Sub engine.
68
+ *
69
+ * @param url [String] Redis-compatible server URL (e.g., "redis://localhost:6379/")
70
+ * @param ping [Integer] Ping interval in seconds (0-255, default: 300)
71
+ * @return [Iodine::PubSub::Engine::RESP3]
72
+ *
73
+ * URL formats supported:
74
+ * - "redis://host:port"
75
+ * - "redis://user:password@host:port/db"
76
+ * - "host:port"
77
+ * - "host" (default port 6379)
78
+ *
79
+ * @example
80
+ * resp3 = Iodine::PubSub::Engine::RESP3.new("redis://localhost:6379/")
81
+ * resp3 = Iodine::PubSub::Engine::RESP3.new("redis://secret@host:6379/", ping: 60)
82
+ */
83
+ static VALUE iodine_redis_initialize(int argc, VALUE *argv, VALUE self) {
84
+ /* Get the engine struct from parent class */
85
+ iodine_pubsub_eng_s *e = iodine_pubsub_eng_get(self);
86
+
87
+ VALUE url_val;
88
+ VALUE opts = Qnil;
89
+
90
+ /* Parse arguments: url, optional keyword args */
91
+ rb_scan_args(argc, argv, "1:", &url_val, &opts);
92
+
93
+ /* Validate URL */
94
+ if (url_val == Qnil || url_val == Qfalse) {
95
+ rb_raise(rb_eArgError, "Redis URL is required");
96
+ return Qnil;
97
+ }
98
+ Check_Type(url_val, T_STRING);
99
+ const char *url = StringValueCStr(url_val);
100
+
101
+ /* Parse ping interval from keyword args */
102
+ uint8_t ping_interval = 0;
103
+ if (opts != Qnil) {
104
+ VALUE ping_val = rb_hash_aref(opts, ID2SYM(rb_intern("ping")));
105
+ if (ping_val != Qnil) {
106
+ long ping = NUM2LONG(ping_val);
107
+ if (ping < 0 || ping > 255) {
108
+ rb_raise(rb_eArgError, "ping must be between 0 and 255");
109
+ return Qnil;
110
+ }
111
+ ping_interval = (uint8_t)ping;
112
+ }
113
+ }
114
+
115
+ /* Create the Redis engine */
116
+ e->ptr = fio_redis_new(.url = url, .ping_interval = ping_interval);
117
+ if (!e->ptr) {
118
+ rb_raise(rb_eRuntimeError, "Failed to create RESP3 engine");
119
+ return Qnil;
120
+ }
121
+
122
+ /* Attach to pub/sub system */
123
+ fio_pubsub_engine_attach(e->ptr);
124
+
125
+ return self;
126
+ }
127
+
128
+ /**
129
+ * @!method connection_state
130
+ * Returns the RESP3 connection state.
131
+ *
132
+ * @return [Symbol] `:connecting`, `:connected`, or `:error`
133
+ */
134
+ static VALUE iodine_redis_connection_state(VALUE self) {
135
+ iodine_pubsub_eng_s *e = iodine_pubsub_eng_get(self);
136
+ switch (fio_redis_state(e->ptr)) {
137
+ case FIO_REDIS_STATE_CONNECTING:
138
+ return ID2SYM(rb_intern("connecting"));
139
+ case FIO_REDIS_STATE_CONNECTED:
140
+ return ID2SYM(rb_intern("connected"));
141
+ default: return ID2SYM(rb_intern("error"));
142
+ }
143
+ }
144
+
145
+ /**
146
+ * @!method cmd(*args, &block)
147
+ * Sends a Redis command and optionally receives the response via callback.
148
+ *
149
+ * @param args [Array<String, Integer, Float, Symbol, Boolean, nil>] Command and arguments
150
+ * @yield [result] Called with the Redis response
151
+ * @yieldparam result [Object] The parsed Redis response
152
+ * @return [Boolean] true on success, false on error
153
+ *
154
+ * @example
155
+ * redis.cmd("SET", "key", "value") { |r| puts "SET result: #{r}" }
156
+ * redis.cmd("GET", "key") { |value| puts "Value: #{value}" }
157
+ * redis.cmd("KEYS", "*") { |keys| p keys }
158
+ * redis.cmd("INCR", "counter") { |new_val| puts new_val }
159
+ *
160
+ * @note Do NOT use SUBSCRIBE/PSUBSCRIBE/UNSUBSCRIBE/PUNSUBSCRIBE commands.
161
+ * These are handled internally by the pub/sub system.
162
+ */
163
+ static VALUE iodine_redis_cmd(int argc, VALUE *argv, VALUE self) {
164
+ iodine_pubsub_eng_s *e = iodine_pubsub_eng_get(self);
165
+
166
+ if (!e->ptr) {
167
+ rb_raise(rb_eRuntimeError, "RESP3 engine not initialized");
168
+ return Qfalse;
169
+ }
170
+
171
+ if (argc < 1) {
172
+ rb_raise(rb_eArgError, "At least one argument (command) is required");
173
+ return Qfalse;
174
+ }
175
+
176
+ /* Build FIOBJ array from Ruby arguments */
177
+ FIOBJ cmd = fiobj_array_new();
178
+ fiobj_array_reserve(cmd, argc);
179
+
180
+ for (int i = 0; i < argc; ++i) {
181
+ VALUE arg = argv[i];
182
+ FIOBJ item = FIOBJ_INVALID;
183
+
184
+ switch (rb_type(arg)) {
185
+ case RUBY_T_STRING:
186
+ item = fiobj_str_new_cstr(RSTRING_PTR(arg), RSTRING_LEN(arg));
187
+ break;
188
+ case RUBY_T_SYMBOL:
189
+ arg = rb_sym2str(arg);
190
+ item = fiobj_str_new_cstr(RSTRING_PTR(arg), RSTRING_LEN(arg));
191
+ break;
192
+ case RUBY_T_FIXNUM:
193
+ item = fiobj_num_new((intptr_t)NUM2LL(arg));
194
+ break;
195
+ case RUBY_T_FLOAT: item = fiobj_float_new(RFLOAT_VALUE(arg)); break;
196
+ case RUBY_T_TRUE: item = fiobj_str_new_cstr("1", 1); break;
197
+ case RUBY_T_FALSE: item = fiobj_str_new_cstr("0", 1); break;
198
+ case RUBY_T_NIL: item = fiobj_null(); break;
199
+ default:
200
+ /* Try to convert to string */
201
+ arg = rb_funcallv(arg, IODINE_TO_S_ID, 0, NULL);
202
+ if (RB_TYPE_P(arg, RUBY_T_STRING)) {
203
+ item = fiobj_str_new_cstr(RSTRING_PTR(arg), RSTRING_LEN(arg));
204
+ } else {
205
+ fiobj_free(cmd);
206
+ rb_raise(rb_eTypeError,
207
+ "Cannot convert argument %d to Redis command element",
208
+ i);
209
+ return Qfalse;
210
+ }
211
+ }
212
+ fiobj_array_push(cmd, item);
213
+ }
214
+
215
+ /* Get the block if provided */
216
+ VALUE block = Qnil;
217
+ if (rb_block_given_p()) {
218
+ block = rb_block_proc();
219
+ }
220
+
221
+ /* Prepare callback context */
222
+ iodine_redis_cmd_ctx_s *ctx = NULL;
223
+ void (*callback)(fio_pubsub_engine_s *, FIOBJ, void *) = NULL;
224
+
225
+ if (block != Qnil) {
226
+ ctx =
227
+ (iodine_redis_cmd_ctx_s *)FIO_MEM_REALLOC(NULL, 0, sizeof(*ctx), 0);
228
+ if (!ctx) {
229
+ fiobj_free(cmd);
230
+ rb_raise(rb_eNoMemError, "Failed to allocate callback context");
231
+ return Qfalse;
232
+ }
233
+ ctx->block = block;
234
+ ctx->engine = e->ptr;
235
+ STORE.hold(block);
236
+ callback = iodine_redis_cmd_callback;
237
+ }
238
+
239
+ /* Send the command */
240
+ int result = fio_redis_send(e->ptr, cmd, callback, ctx);
241
+ fiobj_free(cmd);
242
+
243
+ if (result != 0) {
244
+ if (ctx) {
245
+ STORE.release(block);
246
+ FIO_MEM_FREE(ctx, sizeof(*ctx));
247
+ }
248
+ return Qfalse;
249
+ }
250
+
251
+ return Qtrue;
252
+ }
253
+
254
+ /* *****************************************************************************
255
+ Custom dealloc for Redis - need to call fio_redis_free instead of default
256
+ ***************************************************************************** */
257
+
258
+ static void iodine_redis_free(void *ptr_) {
259
+ iodine_pubsub_eng_s *e = (iodine_pubsub_eng_s *)ptr_;
260
+ if (e->ptr) {
261
+ fio_pubsub_engine_detach(e->ptr);
262
+ fio_redis_free(e->ptr);
263
+ e->ptr = NULL;
264
+ }
265
+ ruby_xfree(e);
266
+ }
267
+
268
+ static const rb_data_type_t IODINE_REDIS_DATA_TYPE = {
269
+ .wrap_struct_name = "IodineRedis",
270
+ .function =
271
+ {
272
+ .dfree = iodine_redis_free,
273
+ .dsize = iodine_pubsub_eng_data_size,
274
+ },
275
+ .parent = &IODINE_PUBSUB_ENG_DATA_TYPE,
276
+ .data = NULL,
277
+ };
278
+
279
+ static VALUE iodine_redis_alloc(VALUE klass) {
280
+ iodine_pubsub_eng_s *e =
281
+ (iodine_pubsub_eng_s *)ruby_xmalloc(sizeof(iodine_pubsub_eng_s));
282
+ if (!e)
283
+ goto no_memory;
284
+ *e = (iodine_pubsub_eng_s){0};
285
+ return TypedData_Wrap_Struct(klass, &IODINE_REDIS_DATA_TYPE, e);
286
+
287
+ no_memory:
288
+ FIO_LOG_FATAL("Memory allocation failed for Redis engine");
289
+ fio_io_stop();
290
+ return Qnil;
291
+ }
292
+
293
+ /* *****************************************************************************
294
+ Initialization
295
+ ***************************************************************************** */
296
+
297
+ /**
298
+ * Initializes the Iodine::PubSub::Engine::RESP3 class.
299
+ */
300
+ static void Init_Iodine_RESP3(void) {
301
+ /* Define Iodine::PubSub::Engine::RESP3 as a subclass of Engine. */
302
+ iodine_rb_IODINE_REDIS = rb_define_class_under(iodine_rb_IODINE_PUBSUB_ENG,
303
+ "RESP3",
304
+ iodine_rb_IODINE_PUBSUB_ENG);
305
+ STORE.hold(iodine_rb_IODINE_REDIS);
306
+
307
+ /* Set up allocation function */
308
+ rb_define_alloc_func(iodine_rb_IODINE_REDIS, iodine_redis_alloc);
309
+
310
+ /* Define instance methods */
311
+ rb_define_method(iodine_rb_IODINE_REDIS,
312
+ "initialize",
313
+ iodine_redis_initialize,
314
+ -1);
315
+ rb_define_method(iodine_rb_IODINE_REDIS,
316
+ "connection_state",
317
+ iodine_redis_connection_state,
318
+ 0);
319
+ rb_define_method(iodine_rb_IODINE_REDIS, "cmd", iodine_redis_cmd, -1);
320
+ }
321
+
322
+ #endif /* H___IODINE_REDIS___H */