iodine 0.7.42 → 0.7.43

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a3ae02e70ea4439584f432552b08cab2cb26ee33c70b2a7263fd56f75bbf7f40
4
- data.tar.gz: 8fa79e4b09b1f7d9d889038a9e091da2632759683d6410c318c1d965f9768b4d
3
+ metadata.gz: 7a444a825ab9cf0e6bd11fe14fb20c6a777e9282c1bea56cf5dc869087ce7a44
4
+ data.tar.gz: 4c698a81975a588e8a5c185d57f9ca76ae1d37200d2a258f5d38631de386e9dd
5
5
  SHA512:
6
- metadata.gz: 063baf66e22412189a923b4f8bb566ab9cfd14b267460bced8a6774abcc4ae9ae028a737a9e4c4963c84c3629e71bd147fb16d24d836b47eee09940a8a30f9a3
7
- data.tar.gz: b16abe85cfebb592c05e9e87be4401fbaac436791489da5aab14199c30b3cfd9bd187ef4a382e2f6c6fc4024949b8b283d0879cf41d63123ca0845327b8b54c5
6
+ metadata.gz: 9659c57e4d0c9f884b52dc07cf7b4c1a707f78d27bf03d058db1c2bfe2c873141a7d128c56b933098fa61a1f8fc48cce1319fea298c15bc8c606bb665b6d7d88
7
+ data.tar.gz: f183fb08856e81963d90d03ba0319f2633a72634beb8b37b2a5cee6c66dfa61710de5532abb5c82dd059d0c8382b9968a824f2caefeee2a53213e2bab2bac0b3
@@ -6,9 +6,15 @@ Please notice that this change log contains changes for upcoming releases as wel
6
6
 
7
7
  ## Changes:
8
8
 
9
+ #### Change log v.0.7.43 (2020-11-03)
10
+
11
+ **Fix**: Fixes an issue where the GVL state in user-spawned threads is inaccurate. This issue only occurs if spawning a new thread and calling certain Iodine methods from a user thread.
12
+
13
+ **Fix**: validate that data passed by the user to `write` is a String object and print warnings / raise exceptions if t isn't. Credit to Vamsi Ambati for asking a question that exposed this issue.
14
+
9
15
  #### Change log v.0.7.42 (2020-08-02)
10
16
 
11
- Fix: Implement fix suggested by @Shelvak (Néstor Coppi) in issue #98.
17
+ **Fix**: Implement fix suggested by @Shelvak (Néstor Coppi) in issue #98.
12
18
 
13
19
  #### Change log v.0.7.41 (2020-07-24)
14
20
 
data/README.md CHANGED
@@ -576,8 +576,8 @@ Iodine is written in C and allows some compile-time customizations, such as:
576
576
  These options can be used, for example, like so:
577
577
 
578
578
  ```bash
579
- $ CFLAGS="-DFIO_FORCE_MALLOC=1 -DHTTP_MAX_HEADER_COUNT=64" \
580
- gem install iodine
579
+ gem install iodine -- \
580
+ --with-cflags=\"-DHTTP_MAX_HEADER_LENGTH=48000 -DFIO_FORCE_MALLOC=1 -DHTTP_MAX_HEADER_COUNT=64\"
581
581
  ```
582
582
 
583
583
  More possible compile time options can be found in the [facil.io documentation](http://facil.io).
@@ -1,66 +1,106 @@
1
- # Ruby Pub/Sub API Specification Draft
1
+ # Ruby pub/sub API Specification Draft
2
+
3
+ ### Draft State
4
+
5
+ This draft is under discussion and will be implemented by iodine starting with the 0.8.x versions.
6
+
7
+ ---
2
8
 
3
9
  ## Purpose
4
10
 
5
- The pub/sub design is idiomatic to WebSocket and EventSource approaches as well as other reactive programming techniques.
11
+ This document details a Rack specification extension for publish/subscribe (pub/sub) modules that can extend WebSocket / EventSource Rack servers.
6
12
 
7
- The purpose of this specification is to offer a recommendation for pub/sub design that will allow applications to be implementation agnostic (not care which pub/sub extension is used)\*.
13
+ The purpose of this specification is:
8
14
 
9
- Simply put, applications will not have to worry about the chosen pub/sub implementation or about inter-process communication.
15
+ 1. To keep separation of concerns by avoiding inter-process-communication logic (IPC) in the application code base.
10
16
 
11
- This should simplify the idiomatic `subscribe` / `publish` approach to real-time data pushing.
17
+ This is important since IPC is often implemented using pipes / sockets, which could introduce network and IO concerns into the application code.
12
18
 
13
- \* The pub/sub extension could be implemented by any external library as long as the API conforms to the specification. The extension will have to manage the fact that some servers `fork` and manage inter-process communication for pub/sub propagation (or limit it's support to specific servers). Also, servers that opt to implement the pub/sub layer, could perform optimizations related to connection handling and pub/sub lifetimes.
19
+ 2. To specify a common publish/subscribe (pub/sub) API for servers and pub/sub modules, allowing applications to easily switch between conforming implementations and servers.
14
20
 
15
- ## Pub/Sub handling
21
+ Since pub/sub design is idiomatic to WebSocket and EventSource approaches, as well as other reactive programming techniques, it is better if applications aren't locked in to a specific server / implementation.
16
22
 
17
- Conforming Pub/Sub implementations **MUST** implement the following pub/sub related methods within the WebSocket/SSE `client` object (as defined in the Rack WebSockets / EventSource specification draft):
23
+ 3. To provide common considerations and guidelines for pub/sub implementors to consider when implementing their pub/sub modules.
18
24
 
19
- * `subscribe(to, opt = {}) { |from, message| optional_block }` where `to` is a named *channel* and `opt` is a Hash object that *SHOULD* support the following possible keys (unsupported keys *MUST* be ignored):
25
+ Some concerns are common for pub/sub implementors, such as integrating third party message brokers (Redis, RabbitMQ, Cassandra)
20
26
 
21
- * `:match` indicates a matching algorithm should be applied to the `to` variable (`to` is a pattern).
22
-
23
- Possible (suggested) values should include [`:redis`](https://github.com/antirez/redis/blob/398b2084af067ae4d669e0ce5a63d3bc89c639d3/src/util.c#L46-L167), [`:nats`](https://nats.io/documentation/faq/#wildcards) or [`:rabbitmq`](https://www.rabbitmq.com/tutorials/tutorial-five-ruby.html). Pub/Sub implementations *MAY* support none, some or all of these common pattern resolution schemes.
24
-
25
- * `:handler` is an alternative to the optional block. It should accept Proc like objects (objects that answer to `.call(from, msg)`).
27
+ ## Pub/Sub Instance Methods
28
+
29
+ Conforming pub/sub implementations **MUST** implement the following pub/sub instance methods:
30
+
31
+ * `pubsub?` **MUST** return the pub/sub API version as an integer number. Currently, set to `0` (development version).
26
32
 
27
- * If an optional `block` (or `:handler`) is provided, if will be called when a publication was received. Otherwise, the message alone (**not** the channel data) **MUST** be sent directly to the WebSocket / EventSource client.
33
+ * `subscribe(to, is_pattern = false) { |from, message| optional_block }` where:
28
34
 
29
- * `:as` accepts either `:text` or `:binary` Symbol objects.
35
+ * `to` is a named **channel** (or **pattern**).
30
36
 
31
- This option is only valid if the optional `block` is missing and the connection is a WebSocket connection. Note that SSE connections are limited to text data by design.
37
+ The implementation **MAY** support pattern matching for named channels (`to`). The pattern matching algorithm used, if any, **SHOULD** be documented.
32
38
 
33
- This will dictate the encoding for outgoing WebSocket message when publications are directly sent to the client (as a text message or a binary blob). `:text` will be the default value for a missing `:as` option.
39
+ If the implementation does **NOT** support pattern matching and `is_pattern` is truthful, the implementation **MUST** raise and exception.
34
40
 
35
- Servers *MAY* ignore this value if they set the message type (text/binary) based on UTF-8 validation.
41
+ `to` **SHOULD** be a String, but implementations **MAY** support Symbols as aliases to Strings (in which case `:my_channel` is the same `'my_channel'`).
36
42
 
37
- If a subscription to `to` already exists, it should be *replaced* by the new subscription (the old subscription should be canceled / unsubscribed).
43
+ * `block` is optional and accepts (if provided) two arguments (`from` which is equal to `to` and `message` which contains the data's content).
38
44
 
39
- When the `subscribe` method is called within a WebSocket / SSE Callback object, the subscription must be closed automatically when the connection is closed.
45
+ `block` (if provided) **MUST** be called when a publication was received by the named channel.
46
+
47
+ If no `block` is provided:
48
+
49
+ * If the pub/sub instance is an extended WebSocket / EventSource (SSE) `client` object (see the WebSocket / EventSource extension draft) data should be directly sent to the `client`.
50
+
51
+ * If the pub/sub isn't linked with a `client` / connection, an exception **MUST** be raised.
52
+
53
+ If a subscription to `to` already exists for the same pub/sub instance, it should be *replaced* by the new subscription (the old subscription should be canceled / unsubscribed).
54
+
55
+ If the pub/sub instance is an extended WebSocket / EventSource (SSE) `client` object (see the WebSocket / EventSource extension draft), the subscription **MUST** be closed automatically when the connection is closed (when `on_close` is called).
56
+
57
+ Otherwise, the subscription **MUST** be closed automatically when the pub/sub object goes out of scope and is garbage collected (if this ever happens).
40
58
 
41
59
  The `subscribe` method **MUST** return `nil` on a known failure (i.e., when the connection is already closed), or any truthful value on success.
42
60
 
43
- A global variation for this method (allowing global subscriptions to be created) **SHOULD** be made available as `Rack::PubSub.subscribe`.
61
+ * `unsubscribe(from, is_pattern = false)` should cancel a subscription to the `from` named channel / pattern.
62
+
63
+ * `publish(to, message, engine = nil)` where:
44
64
 
45
- When the `subscribe` method isn't called from within a connection, it should be considered a global (non connection related) subscription and an exception should be raised if a `block` or `:handler` isn't provided by the user.
65
+ * `to` is a named channel, same as detailed in `subscribe`.
46
66
 
47
- * `unsubscribe(from)` should cancel a subscription to the `from` named channel / pattern.
67
+ Implementations **MAY** support pattern based publishing. This **SHOULD** be documented as well as how patterns are detected (as opposed to named channels). Note that pattern publishing isn't supported by common backends (such as Redis) and introduces complex privacy and security concerns.
48
68
 
49
- * `publish(to, message, engine = nil)` (preferably supporting named arguments) where:
69
+ * `message` a String containing the data to be published.
50
70
 
51
- * `to` a String that identifies the channel / stream / subject for the publication ("channel" is the semantic used by Redis, it is similar to "subject" or "stream" in other pub/sub systems).
71
+ If `message` is NOT a String, the implementation **MAY** convert the data silently to JSON. Otherwise, the implementation **MUST** raise an exception.
52
72
 
53
- * `message` a String with containing the data to be published.
73
+ `message` encoding (binary / UTZ-8) **MAY** be altered during publication, but any change **MUST** result in valid encoding.
54
74
 
55
- * `engine` routes the publish method to the specified Pub/Sub Engine (see later on). If none is specified, the default engine should be used. If `false` is specified, the message should be forwarded to all subscribed clients.
75
+ * `engine` routes the publish method to the specified pub/sub Engine (see later on). If none is specified, the default engine should be used. If `false` is specified, the message **MUST** be forwarded to all subscribed clients on the **same process**.
56
76
 
57
- The `publish` method must return `true` if a publication was scheduled (not necessarily performed). If it's already known that the publication would fail, the method should return `false`.
77
+ The `publish` method **MUST** return `true` if a publication was scheduled (not necessarily performed). If it's already known that the publication would fail, the method should return `false`.
58
78
 
59
79
  An implementation **MUST** call the relevant PubSubEngine's `publish` method after performing any internal book keeping logic. If `engine` is `nil`, the default PubSubEngine should be called. If `engine` is `false`, the implementation **MUST** forward the published message to the actual clients (if any).
60
80
 
61
81
  A global alias for this method (allowing it to be accessed from outside active connections) **MAY** be defined as `Rack::PubSub.publish`.
62
82
 
63
- Implementations **MUST** implement the following methods in one of their public classes / modules (iodine implements these under `Iodine::PubSub`):
83
+ ## Integrating a Pub/Sub module into a WebSocket / EventSource (SSE) `client` object
84
+
85
+ A conforming pub/sub module **MUST** be designed so that it can be integrated into WebSocket / EventSource (SSE) `client` objects be `include`-ing their class.
86
+
87
+ This **MUST** result in a behavior where subscriptions are destroyed / canceled once the `client` object state changes to "closed" - i.e., either when the `on_close` callback is called, or the first time the method `client.open?` returns `false`.
88
+
89
+ The idiomatic way to add a pub/sub module to a `client`'s class is:
90
+
91
+ ```ruby
92
+ client.class.prepend MyPubSubModule unless client.pubsub?
93
+ ```
94
+
95
+ The pub/sub module **MUST** expect and support this behavior.
96
+
97
+ **Note**: the use of `prepend` (rather than `include`) is chosen so that it's possible to override the `client`'s instance method of `pubsub?`.
98
+
99
+ ## Connecting to External Backends (pub/sub Engines)
100
+
101
+ It is common for scaling applications to require an external message broker backend such as Redis, RabbitMQ, etc'. The following requirements set a common interface for such "engine" implementation and integration.
102
+
103
+ Pub/sub implementations **MUST** implement the following module / class methods in one of their public classes / modules (iodine implements these under `Iodine::PubSub`):
64
104
 
65
105
  * `attach(engine)` where `engine` is a `PubSubEngine` object, as described in this specification.
66
106
 
@@ -72,9 +112,9 @@ Implementations **MUST** implement the following methods in one of their public
72
112
 
73
113
  * `detach(engine)` where `engine` is a PubSubEngine object as described in this specification.
74
114
 
75
- Removes an engine from the pub/sub system. The opposite of `attach`.
115
+ The implementation **MUST** remove the engine from the attached engine list. The opposite of `attach`.
76
116
 
77
- * `default = engine` sets a default pub/sub engine, where `engine` is a PubSubEngine object as described in this specification.
117
+ * `default=(engine)` sets a default pub/sub engine, where `engine` is a PubSubEngine object as described in this specification.
78
118
 
79
119
  Implementations **MUST** forward any `publish` method calls to the default pub/sub engine, unless an `engine` is specified in arguments passes to the `publish` method.
80
120
 
@@ -84,34 +124,36 @@ Implementations **MUST** implement the following methods in one of their public
84
124
 
85
125
  Implementations **MUST** behave as if the engine was newly registered and (re)inform the engine of any existing subscriptions by calling engine's `subscribe` callback for each existing subscription.
86
126
 
87
- Implementations **MAY** implement pub/sub internally (in which case the `default` engine is the server itself or a server's module).
127
+ A `PubSubEngine` instance object **MUST** implement the following methods:
88
128
 
89
- However, servers **MUST** support external pub/sub "engines" as described above, using PubSubEngine objects.
129
+ * `subscribe(to, is_pattern = false)` this method informs the engine that a subscription to the specified channel / pattern exists for the calling the process. It **MUST ONLY** be called **once** for all existing and future subscriptions to that channel within the process.
90
130
 
91
- `PubSubEngine` objects **MUST** implement the following methods:
131
+ The method **MUST** return `true` if a subscription was scheduled (or performed) or `false` if the subscription is known to fail.
92
132
 
93
- * `subscribe(channel, match=nil)` this method performs the subscription to the specified channel.
133
+ This method will be called by the pub/sub implementation (for each registered engine). The engine **MAY** assume that the method would never be called directly by an application / client.
94
134
 
95
- If `match` is a Symbol that the engine recognizes (i.e., `:redis`, `:nats`, etc'), the engine should behave accordingly. i.e., the value `:redis` on a Redis engine will invoke the PSUBSCRIBE Redis command.
135
+ This method **MUST NOT** raise an exception.
96
136
 
97
- The method must return `true` if a subscription was scheduled (or performed) or `false` if the subscription is known to fail.
137
+ * `unsubscribe(from, is_pattern = false)` this method informs an engine that there are no more subscriptions to the named channel / pattern for the calling process.
98
138
 
99
- This method will be called by the server (for each registered engine). The engine may assume that the method would never be called directly by an application.
139
+ The method's semantics are similar to `subscribe` only is performs the opposite action.
100
140
 
101
- * `unsubscribe(channel, match=nil)` this method performs closes the subscription to the specified channel.
141
+ This method **MUST NOT** raise an exception.
102
142
 
103
- The method's semantics are similar to `subscribe`.
143
+ This method will be called by the pub/sub implementation (for each registered engine). The engine **MAY** assume that the method would never be called directly by an application / client.
104
144
 
105
- This method will be called by the server (for each registered engine). The engine may assume that the method would never be called directly by an application.
106
-
107
- * `publish(channel, message)` where both `channel` and `message` are String objects.
145
+ * `publish(channel, message)` where both `channel` is either a Symbol or a String (both being equivalent) and `message` **MUST** be a String.
108
146
 
109
147
  This method will be called by the server when a message is published using the engine.
110
148
 
111
- The engine **MUST** assume that the method might get called directly by an application.
149
+ This method will be called by the pub/sub implementation (for each registered engine).
150
+
151
+ The engine **MUST** assume that the method **MAY** be called directly by an application / client.
152
+
153
+ In order for a PubSubEngine instance object to publish messages to all subscribed clients on a particular process, it **SHOULD** call the implementation's global `publish` method with the engine set to `false`.
112
154
 
113
- When a PubSubEngine object receives a published message, it *should* call:
155
+ i.e., if the implementation's global `publish` method is in a class called `Iodine`:
114
156
 
115
157
  ```ruby
116
- Foo::PubSub.publish channel, message, false
158
+ Iodine.publish channel, message, false
117
159
  ```
@@ -5,62 +5,72 @@ This draft is also implemented by [the Agoo server](https://github.com/ohler55/a
5
5
  ---
6
6
  ## Purpose
7
7
 
8
- This document details WebSocket / EventSource connection support for Rack servers.
8
+ This document details a Rack specification extension for WebSocket / EventSource servers.
9
9
 
10
- The purpose of these specifications is:
10
+ The purpose of this specification is:
11
11
 
12
- 1. To allow separation of concerns between the transport layer and the application, thereby allowing the application to be server agnostic.
12
+ 1. To improve application safety by phasing out the use of `hijack` and replacing it with the use of application object callbacks.
13
13
 
14
- Simply put, when choosing between conforming servers, the application doesn’t need to have any knowledge about the chosen server.
14
+ This should make it easer for applications to accept WebSocket and EventSource (SSE) connections without exposing themselves to risks and errors related to IO / network logic (such as slow client attacks, buffer flooding, etc').
15
15
 
16
- 2. To support “native" (server-side) WebSocket and EventSource (SSE) connections and using application side callbacks.
16
+ 2. To improve separation of concerns between servers and applications, moving the IO / network logic related to WebSocket and EventSource (SSE) back to the server.
17
17
 
18
- Simply put, to make it easy for applications to accept WebSocket and EventSource (SSE) connections from WebSocket and EventSource clients (commonly browsers) while abstracting away any transport layer details.
19
-
20
- 3. Allow applications to use WebSocket and EventSource (SSE) on HTTP/2 servers. Note: current `hijack` practices will break network connections when attempting to implement EventSource (SSE).
18
+ Simply put, when using a server that support this extension, the application / framework doesn’t need to have any knowledge about networking, transport protocols, IO streams, polling, etc'.
21
19
 
22
20
  ## Rack WebSockets / EventSource
23
21
 
24
- Servers that publish WebSocket and/or EventSource (SSE) support using the `env['rack.upgrade?']` value MUST follow the requirements set in this document.
22
+ Servers that publish WebSocket and/or EventSource (SSE) support using the `env['rack.upgrade?']` value **MUST** follow the requirements set in this document.
25
23
 
26
24
  This document reserves the Rack `env` Hash keys of `rack.upgrade?` and `rack.upgrade`.
27
25
 
28
- A conforming server MUST set `env['rack.upgrade?']` to `:websocket` for incoming WebSocket connections and `:sse` for incoming EventSource (SSE) connections.
26
+ A conforming server **MUST** set `env['rack.upgrade?']` to `:websocket` for incoming WebSocket connections and `:sse` for incoming EventSource (SSE) connections.
27
+
28
+ If a connection is not "upgradeable", a conforming server **SHOULD** set `env['rack.upgrade?']` to either `nil` or `false`. Setting the `env['rack.upgrade?']` to either `false` or `nil` should make it easier for applications to test for server support during a normal HTTP request.
29
+
30
+ If the connection is upgradeable and a client application set a value for `env['rack.upgrade']`:
31
+
32
+ * the server **MUST** use that value as a WebSocket / EventSource Callback Object unless the response status code `>= 300` (redirection / error status code).
33
+
34
+ * The response `body` **MUST NOT** be sent when switching to a Callback Object.
35
+
36
+ If a connection is **NOT** upgradeable and a client application set a value for `env['rack.upgrade']`:
29
37
 
30
- If a connection is not "upgradable", a conforming server SHOULD set `env['rack.upgrade?']` to either `nil` or `false`.
38
+ * The server **SHOULD** ignore the Callback Object and process the response as if it did not exist.
31
39
 
32
- The historical use of `upgrade.websocket?` and `upgrade.websocket` (iodine 0.4.x) will be gradually deprecated.
40
+ * A server **MAY** use the Callback Object to allow a client to "hijack" the data stream as raw data stream. Such behavior **MUST** be documented.
33
41
 
34
42
  ### The WebSocket / EventSource Callback Object
35
43
 
36
44
  WebSocket and EventSource connection upgrade and handling is performed using a Callback Object.
37
45
 
38
- The Callback Object could be a any object which implements any of the following callbacks:
46
+ The Callback Object could be a any object which implements any (of none) of the following callbacks:
39
47
 
40
- * `on_open(client)` WILL be called once the connection had been established.
48
+ * `on_open(client)` **MUST** be called once the connection had been established and/or the Callback Object had been linked to the `client` object.
41
49
 
42
- * `on_message(client, data)` WILL be called when incoming WebSocket data is received.
50
+ * `on_message(client, data)` **MUST** be called when incoming WebSocket data is received.
43
51
 
44
52
  This callback is ignored for EventSource connections.
45
53
 
46
- `data` will be a String with an encoding of UTF-8 for text messages and `binary` encoding for non-text messages (as specified by the WebSocket Protocol).
54
+ `data` **MUST** be a String with an encoding of UTF-8 for text messages and `binary` encoding for non-text messages (as specified by the WebSocket Protocol).
47
55
 
48
56
  The *callback object* **MUST** assume that the `data` String will be a **recyclable buffer** and that it's content will be corrupted the moment the `on_message` callback returns.
49
57
 
50
- Servers **MAY**, optionally, implement a **recyclable buffer** for the `on_message` callback. However, this is optional and it is *not* required.
58
+ Servers **MAY**, optionally, implement a **recyclable buffer** for the `on_message` callback. However, this is optional, is *not* required and might result in issues in cases where the client code is less than pristine.
51
59
 
52
- * `on_drained(client)` **MAY** be called when the the `client.write` buffer becomes empty. **If** `client.pending` returns a non-zero value, the `on_drained` callback **MUST** be called once the write buffer becomes empty.
60
+ * `on_drained(client)` **MAY** be called when the `client.write` buffer becomes empty. **If** `client.pending` ever returns a non-zero value (see later on), the `on_drained` callback **MUST** be called once the write buffer becomes empty.
53
61
 
54
62
  * `on_shutdown(client)` **MAY** be called during the server's graceful shutdown process, _before_ the connection is closed and in addition to the `on_close` function (which is called _after_ the connection is closed.
55
63
 
56
- * `on_close(client)` **MUST** be called _after_ the connection was closed for whatever reason (socket errors, parsing errors, timeouts, client disconnection, `client.close` being called, etc').
64
+ * `on_close(client)` **MUST** be called _after_ the connection was closed for whatever reason (socket errors, parsing errors, timeouts, client disconnection, `client.close` being called, etc') or the Callback Object was replaced by another Callback Object.
57
65
 
58
66
 
59
67
  The server **MUST** provide the Callback Object with a `client` object, that supports the following methods (this approach promises applications could be server agnostic):
60
68
 
61
- * `write(data)` will schedule the data to be sent. `data` **MUST** be a String.
69
+ * `env` **MUST** return the Rack `env` hash related to the originating HTTP request. Some changes to the `env` hash (such as removal of the IO hijacking support) **MAY** be implemented by the server.
62
70
 
63
- A call to `write` only promises that the data is scheduled to be sent. Servers are encouraged to avoid blocking and return immediately, deferring the actual `write` operation for later.
71
+ * `write(data)` **MUST** schedule **all** the data to be sent. `data` **MUST** be a String. Servers **MAY** silently convert non-String objects to JSON if an application attempts to `write` a non-String value, otherwise servers **SHOULD** throw an exception.
72
+
73
+ A call to `write` only promises that the data is scheduled to be sent. Implementation details may differ across servers.
64
74
 
65
75
  `write` shall return `true` on success and `false` if the connection is closed.
66
76
 
@@ -70,27 +80,52 @@ The server **MUST** provide the Callback Object with a `client` object, that sup
70
80
 
71
81
  * If `data` is binary encoded it will be sent as non-text (as specified by the WebSocket Protocol).
72
82
 
73
- A server **SHOULD** document whether `write` will block. It is **RECOMMENDED** that servers implement buffered IO, allowing `write` to return immediately when resources allow and block (or, possibly, disconnect) when the IO buffer is full.
83
+ A server **SHOULD** document its concurrency model, allowing developers to know whether `write` will block or not, whether buffered IO is implemented, etc'.
84
+
85
+ For example, evented servers are encouraged to avoid blocking and return immediately, deferring the actual `write` operation for later. However, (process/thread/fiber) per-connection based servers **MAY** choose to return only after all the data was sent. Documenting these differences will allows applications to choose the model that best fits their needs and environments.
74
86
 
75
- * `close` closes the connection once all the data in the outgoing queue was sent. If `close` is called while there is still data to be sent, `close` will only take effect once the data was sent.
87
+ * `close` closes the connection once all the data scheduled using `write` was sent. If `close` is called while there is still data to be sent, `close` **SHOULD** return immediately and only take effect once the data was sent.
76
88
 
77
89
  `close` shall always return `nil`.
78
90
 
79
- * `open?` returns `true` if the connection isn't known to have been closed and `false` if the connection is known to be closed or marked to be closed.
91
+ * `open?` **MUST** return `false` **if** the connection was never open, is known to be closed or marked to be closed. Otherwise `true` **MUST** be returned.
80
92
 
81
93
  * `pending` **MUST** return -1 if the connection is closed. Otherwise, `pending` **SHOULD** return the number of pending writes (messages in the `write` queue\*) that need to be processed before the next time the `on_drained` callback is called.
82
94
 
83
- Servers **MAY** choose to always return the value `0` if they never call the `on_drained` callback and the connection is open.
95
+ Servers **MAY** choose to always return the value `0` **ONLY IF** they never call the `on_drained` callback and the connection is open.
96
+
97
+ Servers that return a positive number **MUST** call the `on_drained` callback when a call to `pending` would return the value `0`.
98
+
99
+ \*Servers that divide large messages into a number of smaller messages (implement message fragmentation) **MAY** count each fragment separately, as if the fragmentation was performed by the user and `write` was called more than once per message.
100
+
101
+ * `pubsub?` **MUST** return `false` **unless** the pub/sub extension is supported.
102
+
103
+ Pub/Sub patterns are idiomatic for WebSockets and EventSource connections but their API is out of scope for this extension.
84
104
 
85
- Servers that return a positive number MUST call the `on_drained` callback when a call to `pending` would return the value `0`.
105
+ * `class` **MUST** return the client's Class, allowing it be extended with additional features (such as Pub/Sub, etc').
86
106
 
87
- \*Servers that divide large messages into a number of smaller messages (implement message fragmentation) MAY count each fragment separately, as if the fragmentation was performed by the user and `write` was called more than once per message.
107
+ **Note**: Ruby adds this method automatically to every class, no need to do a thing.
88
108
 
89
- * `env` returns the Rack `env` hash related to the originating HTTP request. Some changes to the `env` hash (such as removal of the IO hijacking support) MAY be implemented by the server.
109
+ The server **MAY** support the following (optional) methods for the `client` object:
90
110
 
91
- WebSocket `ping` / `pong`, timeouts and network considerations should be implemented by the server. It is **RECOMMENDED** (but not required) that the server send `ping`s to prevent connection timeouts and detect network failure.
111
+ * `handler` if implemented, **MUST** return the callback object linked to the `client` object.
92
112
 
93
- Server settings **MAY** (not required) be provided to allow for customization and adaptation for different network environments or WebSocket extensions. It is **RECOMMENDED** that any settings be available as command line arguments and **not** incorporated into the application's logic.
113
+ * `handler=` if implemented, **MUST** set a new Callback Object for `client`.
114
+
115
+ This allows applications to switch from one callback object to another (i.e., in case of credential upgrades).
116
+
117
+ Once a new Callback Object was set, the server **MUST** call the old handler's `on_close` callback and **afterwards** call the new handler's `on_open` callback.
118
+
119
+ It is **RECOMMENDED** (but not required) that this also updates the value for `env['rack.upgrade']`.
120
+
121
+ * `timeout` / `timeout=` allows applications to get / set connection timeouts dynamically and separately for each connection. Servers **SHOULD** provide a global setting for the default connection timeout. It is **RECOMMENDED** (but not required) that a global / default timeout setting be available from the command line (CLI).
122
+
123
+ * `protocol` if implemented, **MUST** return the same value that was originally set by `env['rack.upgrade?']`.
124
+
125
+
126
+ WebSocket `ping` / `pong`, timeouts and network considerations **SHOULD** be implemented by the server. It is **RECOMMENDED** (but not required) that the server send `ping`s to prevent connection timeouts and to detect network failure. Clients **SHOULD** also consider sending `ping`s to detect network errors (dropped connections).
127
+
128
+ Server settings **MAY** be provided to allow for customization and adaptation for different network environments or WebSocket extensions. It is **RECOMMENDED** that any settings be available as command line arguments and **not** incorporated into the application's logic.
94
129
 
95
130
  ---
96
131
 
@@ -100,62 +135,65 @@ Server settings **MAY** (not required) be provided to allow for customization an
100
135
 
101
136
  * **Server**:
102
137
 
138
+ When a regular HTTP request arrives (non-upgradeable), the server will set the `env['rack.upgrade?']` flag to `false`, indicating that: 1. this specific request is NOT upgradable; and 2. the server supports this specification for either WebSocket and/or EventSource connections.
139
+
103
140
  When a WebSocket upgrade request arrives, the server will set the `env['rack.upgrade?']` flag to `:websocket`, indicating that: 1. this specific request is upgradable; and 2. the server supports this specification for WebSocket connections.
104
141
 
105
- When an EventSource request arrives, the server will set the `env['rack.upgrade?']` flag to `:sse`, indicating that: 1. this specific request is an EventSource request; and 2. the server supports this specification.
142
+ When an EventSource request arrives, the server will set the `env['rack.upgrade?']` flag to `:sse`, indicating that: 1. this specific request is an EventSource request; and 2. the server supports this specification for EventSource connections.
106
143
 
107
144
  * **Client**:
108
145
 
109
- When a client decides to upgrade a request, they will place an appropriate Callback Object in the `env['rack.upgrade']` Hash key.
146
+ If a client decides to upgrade a request, they will place an appropriate Callback Object in the `env['rack.upgrade']` Hash key.
110
147
 
111
148
  * **Server**:
112
149
 
113
- 1. The server will review the `env` Hash *before* sending the response. If the `env['rack.upgrade']` was set, the server will perform the upgrade.
150
+ 1. If the application's response status indicates an error or a redirection (status code `>= 300`), the server shall ignore the Callback Object and/or remove it from the `env` Hash, ignoring the rest of the steps that follow.
114
151
 
115
- 2. The server will send the correct response status and headers, as well as any headers present in the response object. The server will also perform any required housekeeping, such as closing the response body, if it exists.
152
+ 2. The server will review the `env` Hash *before* sending the response. If the `env['rack.upgrade']` was set, the server will perform the upgrade.
116
153
 
117
- The response status provided by the response object shall be ignored and the correct response status shall be set by the server.
154
+ 3. The server will send the correct response status and headers, as well as any headers present in the response object. The server will also perform any required housekeeping, such as closing the response body, if it exists.
118
155
 
119
- If the application's response status indicates an error or a redirection (status code >= 300), the server shall ignore the Callback Object.
156
+ The response status provided by the response object shall be ignored and the correct response status shall be set by the server.
120
157
 
121
- 3. Once the upgrade had completed, the server will call the `on_open` callback.
158
+ 4. Once the upgrade had completed, the server will call the `on_open` callback.
122
159
 
123
160
  No other callbacks shall be called until the `on_open` callback had returned.
124
161
 
125
- WebSocket messages shall be handled by the `on_message` callback in the same order in which they arrive and the `on_message` will **not** be executed concurrently for the same connection.
162
+ WebSocket messages shall be handled by the `on_message` callback in the same order in which they arrive and the `on_message` **SHOULD NOT** be executed concurrently for the same connection.
126
163
 
127
- The `on_close` callback will **not** be called while any other callback is running (`on_open`, `on_message`, `on_drained`, etc').
164
+ The `on_close` callback **MUST NOT** be called while any other callback is running (`on_open`, `on_message`, `on_drained`, etc').
128
165
 
129
- The `on_drained` callback might be called concurrently with the `on_message` callback, allowing data to be sent even while other data is being processed. Multi-threading considerations may apply.
166
+ The `on_drained` callback **MAY** be called concurrently with the `on_message` callback, allowing data to be sent even while incoming data is being processed. Multi-threading considerations apply.
130
167
 
131
168
  ## Example Usage
132
169
 
133
170
  The following is an example WebSocket echo server implemented using this specification:
134
171
 
135
172
  ```ruby
136
- class WSConnection
173
+ module WSConnection
137
174
  def on_open(client)
138
- puts "WebSocket connection established."
175
+ puts "WebSocket connection established (#{client.object_id})."
139
176
  end
140
177
  def on_message(client, data)
141
- client.write data
178
+ client.write data # echo the data back
142
179
  puts "on_drained MUST be implemented if #{ pending } != 0."
143
180
  end
144
181
  def on_drained(client)
145
- puts "Yap,on_drained is implemented."
182
+ puts "If this line prints out, on_drained is supported by the server."
146
183
  end
147
184
  def on_shutdown(client)
148
185
  client.write "The server is going away. Goodbye."
149
186
  end
150
187
  def on_close(client)
151
- puts "WebSocket connection closed."
188
+ puts "WebSocket connection closed (#{client.object_id})."
152
189
  end
190
+ extend self
153
191
  end
154
192
 
155
193
  module App
156
194
  def self.call(env)
157
195
  if(env['rack.upgrade?'.freeze] == :websocket)
158
- env['rack.upgrade'.freeze] = WSConnection.new
196
+ env['rack.upgrade'.freeze] = WSConnection
159
197
  return [0, {}, []]
160
198
  end
161
199
  return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
@@ -165,23 +203,22 @@ end
165
203
  run App
166
204
  ```
167
205
 
168
- The following example uses Push notifications for both WebSocket and SSE connections. The Pub/Sub API isn't part of this specification but it is supported by iodine:
206
+ The following example uses Push notifications for both WebSocket and SSE connections. The Pub/Sub API is subject to a separate Pub/Sub API extension and isn't part of this specification (it is, however, supported by iodine):
169
207
 
170
208
  ```ruby
171
- class Chat
172
- def initialize(nickname)
173
- @nickname = nickname
174
- end
209
+ module Chat
175
210
  def on_open(client)
211
+ client.class.prepend MyPubSubModule unless client.pubsub?
176
212
  client.subscribe "chat"
177
- client.publish "chat", "#{@nickname} joined the chat."
213
+ client.publish "chat", "#{env[:nickname]} joined the chat."
178
214
  end
179
215
  def on_message(client, data)
180
- client.publish "chat", "#{@nickname}: #{data}"
216
+ client.publish "chat", "#{env[:nickname]}: #{data}"
181
217
  end
182
218
  def on_close(client)
183
- client.publish "chat", "#{@nickname}: left the chat."
219
+ client.publish "chat", "#{env[:nickname]}: left the chat."
184
220
  end
221
+ extend self
185
222
  end
186
223
 
187
224
  module App
@@ -189,7 +226,7 @@ module App
189
226
  if(env['rack.upgrade?'.freeze])
190
227
  nickname = env['PATH_INFO'][1..-1]
191
228
  nickname = "Someone" if nickname == "".freeze
192
- env['rack.upgrade'.freeze] = Chat.new(nickname)
229
+ env[:nickname] = nickname
193
230
  return [0, {}, []]
194
231
  end
195
232
  return [200, {"Content-Length" => "12", "Content-Type" => "text/plain"}, ["Hello World!"]]
@@ -0,0 +1,92 @@
1
+ # This is a task scheduling WebSocket push example application.
2
+ #
3
+ # Benchmark HTTPP with `ab` or `wrk` (a 5 seconds benchmark with 2000 concurrent clients):
4
+ #
5
+ # ab -c 2000 -t 5 -n 1000000 -k http://127.0.0.1:3000/
6
+ # wrk -c2000 -d5 -t12 http://localhost:3000/
7
+ #
8
+ # Test websocket tasks using the browser. For example:
9
+ # ws = new WebSocket("ws://localhost:3000/userID"); ws.onmessage = function(e) {console.log(e.data);}; ws.onclose = function(e) {console.log("closed")};
10
+ # ws.onopen = function(e) {ws.send(JSON.stringify({'task': 'echo', 'data': 'Hello!'}));};
11
+ require 'iodine'
12
+ require 'json'
13
+
14
+ TASK_PUBLISHING_ENGINE = Iodine::PubSub::PROCESS
15
+
16
+ # This module handles tasks and send them back to the frontend
17
+ module TaskHandler
18
+ def echo msg
19
+ msg = Iodine::JSON.parse(msg, symbolize_names: true)
20
+ publish_to = msg.delete(:from)
21
+ Iodine.publish(publish_to, msg.to_json, TASK_PUBLISHING_ENGINE) if publish_to
22
+ puts "performed 'echo' task"
23
+ rescue => e
24
+ puts "JSON task message error? #{e.message} - under attack?"
25
+ end
26
+
27
+ def add msg
28
+ msg = Iodine::JSON.parse(msg, symbolize_names: true)
29
+ raise "addition task requires an array of numbers" unless msg[:data].is_a?(Array)
30
+ msg[:data] = msg[:data].inject(0){|sum,x| sum + x }
31
+ publish_to = msg.delete(:from)
32
+ Iodine.publish(publish_to, msg.to_json, TASK_PUBLISHING_ENGINE) if publish_to
33
+ puts "performed 'add' task"
34
+ rescue => e
35
+ puts
36
+ "JSON task message error? #{e.message} - under attack?"
37
+ end
38
+
39
+ def listen2tasks
40
+ Iodine.subscribe(:echo) {|ch,msg| TaskHandler.echo(msg) }
41
+ Iodine.subscribe(:add) {|ch,msg| TaskHandler.add(msg) }
42
+ end
43
+
44
+ extend self
45
+ end
46
+
47
+ module WebsocketClient
48
+ def on_open client
49
+ # Pub/Sub directly to the client (or use a block to process the messages)
50
+ client.subscribe client.env['PATH_INFO'.freeze]
51
+ end
52
+ def on_message client, data
53
+ # Strings and symbol channel names are equivalent.
54
+ msg = Iodine::JSON.parse(data, symbolize_names: true)
55
+ raise "no valid task" unless ["echo".freeze, "add".freeze].include? msg[:task]
56
+ msg[:from] = client.env['PATH_INFO'.freeze]
57
+ client.publish msg[:task], msg.to_json, TASK_PUBLISHING_ENGINE
58
+ rescue => e
59
+ puts "JSON message error? #{e.message}\n\t#{data}\n\t#{msg}"
60
+ end
61
+ extend self
62
+ end
63
+
64
+ APP = Proc.new do |env|
65
+ if env['rack.upgrade?'.freeze] == :websocket
66
+ env['rack.upgrade'.freeze] = WebsocketClient
67
+ [0,{}, []] # It's possible to set cookies for the response.
68
+ elsif env['rack.upgrade?'.freeze] == :sse
69
+ puts "SSE connections can only receive data from the server, the can't write."
70
+ env['rack.upgrade'.freeze] = WebsocketClient
71
+ [0,{}, []] # It's possible to set cookies for the response.
72
+ else
73
+ [200, {"Content-Type" => "text/plain"}, ["Send messages with WebSockets using JSON.\ni.e.: {\"task\":\"add\", \"data\":[1,2]}"]]
74
+ end
75
+ end
76
+
77
+ # test automatically for Redis extensions.
78
+ if(Iodine::PubSub.default.is_a? Iodine::PubSub::Redis)
79
+ TASK_PUBLISHING_ENGINE = Iodine::PubSub.default
80
+ if(ARGV.include? "worker")
81
+ TaskHandler.listen2tasks
82
+ Iodine.workers = 1
83
+ Iodine.threads = 16 if Iodine.threads == 0
84
+ Iodine.start
85
+ exit(0)
86
+ end
87
+ else
88
+ TaskHandler.listen2tasks
89
+ end
90
+
91
+ # # or in config.ru
92
+ run APP
@@ -33,6 +33,7 @@ VALUE IodineBaseModule;
33
33
  VALUE iodine_default_args;
34
34
 
35
35
  ID iodine_call_id;
36
+ ID iodine_to_s_id;
36
37
 
37
38
  static VALUE address_sym;
38
39
  static VALUE app_sym;
@@ -1302,6 +1303,7 @@ void Init_iodine(void) {
1302
1303
  IodineBaseModule = rb_define_module_under(IodineModule, "Base");
1303
1304
  VALUE IodineCLIModule = rb_define_module_under(IodineBaseModule, "CLI");
1304
1305
  iodine_call_id = rb_intern2("call", 4);
1306
+ iodine_to_s_id = rb_intern("to_s");
1305
1307
 
1306
1308
  // register core methods
1307
1309
  rb_define_module_function(IodineModule, "threads", iodine_threads_get, 0);
@@ -53,6 +53,7 @@ extern VALUE IodineModule;
53
53
  extern VALUE IodineBaseModule;
54
54
  extern VALUE iodine_default_args;
55
55
  extern ID iodine_call_id;
56
+ extern ID iodine_to_s_id;
56
57
 
57
58
  #define IODINE_RSTRINFO(rstr) \
58
59
  ((fio_str_info_s){.len = RSTRING_LEN(rstr), .data = RSTRING_PTR(rstr)})
@@ -5,7 +5,7 @@
5
5
 
6
6
  #include <fio.h>
7
7
 
8
- static __thread volatile uint8_t iodine_GVL_state;
8
+ static __thread volatile uint8_t iodine_GVL_state = 1;
9
9
 
10
10
  /* *****************************************************************************
11
11
  Calling protected Ruby methods
@@ -159,7 +159,7 @@ Ruby Connection Methods - write, close open? pending
159
159
  ***************************************************************************** */
160
160
 
161
161
  /**
162
- * Writes data to the connection asynchronously.
162
+ * Writes data to the connection asynchronously. `data` MUST be a String.
163
163
  *
164
164
  * In effect, the `write` call does nothing, it only schedules the data to be
165
165
  * sent and marks the data as pending.
@@ -174,6 +174,16 @@ static VALUE iodine_connection_write(VALUE self, VALUE data) {
174
174
  return Qnil;
175
175
  // rb_raise(rb_eIOError, "Connection closed or invalid.");
176
176
  }
177
+ if (!RB_TYPE_P(data, T_STRING)) {
178
+ VALUE tmp = data;
179
+ data = IodineCaller.call(data, iodine_to_s_id);
180
+ if (!RB_TYPE_P(data, T_STRING))
181
+ Check_Type(tmp, T_STRING);
182
+ rb_backtrace();
183
+ FIO_LOG_WARNING(
184
+ "`Iodine::Connection#write` was called with a non-String object.");
185
+ }
186
+
177
187
  switch (c->info.type) {
178
188
  case IODINE_CONNECTION_WEBSOCKET:
179
189
  /* WebSockets*/
@@ -234,6 +244,14 @@ static VALUE iodine_connection_is_open(VALUE self) {
234
244
  }
235
245
  return Qfalse;
236
246
  }
247
+
248
+ /**
249
+ * Always returns true, since Iodine connections support the pub/sub extension.
250
+ */
251
+ static VALUE iodine_connection_is_pubsub(VALUE self) {
252
+ return INT2NUM(0);
253
+ (void)self;
254
+ }
237
255
  /**
238
256
  * Returns the number of pending `write` operations that need to complete
239
257
  * before the next `on_drained` callback is called.
@@ -675,13 +693,6 @@ The method accepts an optional `engine` argument:
675
693
 
676
694
  publish(to, message, my_pubsub_engine)
677
695
 
678
-
679
- Alternatively, accepts the following named arguments:
680
-
681
- - `:to` - The channel to publish to (required).
682
- - `:message` - The message to be published (required).
683
- - `:engine` - If provided, the engine to use for pub/sub. Otherwise the default engine is used.
684
-
685
696
  */
686
697
  static VALUE iodine_pubsub_publish(int argc, VALUE *argv, VALUE self) {
687
698
  // clang-format on
@@ -903,6 +914,7 @@ void iodine_connection_init(void) {
903
914
  0);
904
915
  rb_define_method(ConnectionKlass, "handler=", iodine_connection_handler_set,
905
916
  1);
917
+ rb_define_method(ConnectionKlass, "pubsub?", iodine_connection_is_pubsub, 0);
906
918
  rb_define_method(ConnectionKlass, "subscribe", iodine_pubsub_subscribe, -1);
907
919
  rb_define_method(ConnectionKlass, "unsubscribe", iodine_pubsub_unsubscribe,
908
920
  1);
@@ -51,7 +51,6 @@ static VALUE hijack_func_sym;
51
51
  static ID close_method_id;
52
52
  static ID each_method_id;
53
53
  static ID attach_method_id;
54
- static ID iodine_to_s_method_id;
55
54
  static ID iodine_call_proc_id;
56
55
 
57
56
  static VALUE env_template_no_upgrade;
@@ -294,9 +293,9 @@ static int iodine_copy2env_task(FIOBJ o, void *env_) {
294
293
 
295
294
  } else {
296
295
  /* it's an array */
297
- VALUE ary = rb_ary_new();
298
- rb_hash_aset(env, hname, ary);
299
296
  size_t count = fiobj_ary_count(o);
297
+ VALUE ary = rb_ary_new2(count);
298
+ rb_hash_aset(env, hname, ary);
300
299
  for (size_t i = 0; i < count; ++i) {
301
300
  tmp = fiobj_obj2cstr(fiobj_ary_index(o, i));
302
301
  rb_ary_push(ary, rb_enc_str_new(tmp.data, tmp.len, IodineBinaryEncoding));
@@ -476,11 +475,11 @@ static int for_each_header_data(VALUE key, VALUE val, VALUE h_) {
476
475
  http_s *h = (http_s *)h_;
477
476
  // fprintf(stderr, "For_each - headers\n");
478
477
  if (TYPE(key) != T_STRING)
479
- key = IodineCaller.call(key, iodine_to_s_method_id);
478
+ key = IodineCaller.call(key, iodine_to_s_id);
480
479
  if (TYPE(key) != T_STRING)
481
480
  return ST_CONTINUE;
482
481
  if (TYPE(val) != T_STRING) {
483
- val = IodineCaller.call(val, iodine_to_s_method_id);
482
+ val = IodineCaller.call(val, iodine_to_s_id);
484
483
  if (TYPE(val) != T_STRING)
485
484
  return ST_STOP;
486
485
  }
@@ -1130,7 +1129,6 @@ void iodine_init_http(void) {
1130
1129
  close_method_id = rb_intern("close");
1131
1130
  each_method_id = rb_intern("each");
1132
1131
  attach_method_id = rb_intern("attach_fd");
1133
- iodine_to_s_method_id = rb_intern("to_s");
1134
1132
  iodine_call_proc_id = rb_intern("call");
1135
1133
 
1136
1134
  IodineUTF8Encoding = rb_enc_find("UTF-8");
@@ -8,7 +8,6 @@
8
8
  #include <fio.h>
9
9
 
10
10
  static ID call_func_id;
11
- static ID to_s_func_id;
12
11
  static VALUE filename_id;
13
12
  static VALUE data_id;
14
13
  static VALUE template_id;
@@ -169,7 +168,7 @@ static int mustache_on_arg(mustache_section_s *section, const char *name,
169
168
  if (rb_respond_to(o, call_func_id))
170
169
  o = IodineCaller.call(o, call_func_id);
171
170
  if (!RB_TYPE_P(o, T_STRING))
172
- o = IodineCaller.call(o, to_s_func_id);
171
+ o = IodineCaller.call(o, iodine_to_s_id);
173
172
  }
174
173
  if (!RB_TYPE_P(o, T_STRING) || !RSTRING_LEN(o))
175
174
  return 0;
@@ -220,7 +219,7 @@ static int32_t mustache_on_section_test(mustache_section_s *section,
220
219
  }
221
220
  o = IodineCaller.call2(o, call_func_id, 1, &str);
222
221
  if (!RB_TYPE_P(o, T_STRING))
223
- o = rb_funcall2(o, to_s_func_id, 0, NULL);
222
+ o = rb_funcall2(o, iodine_to_s_id, 0, NULL);
224
223
  if (RB_TYPE_P(o, T_STRING) && RSTRING_LEN(o))
225
224
  mustache_write_text(section, RSTRING_PTR(o), RSTRING_LEN(o), 0);
226
225
  return 0;
@@ -553,7 +552,6 @@ Initialize Iodine::Mustache
553
552
 
554
553
  void iodine_init_mustache(void) {
555
554
  call_func_id = rb_intern2("call", 4);
556
- to_s_func_id = rb_intern2("to_s", 4);
557
555
  filename_id = rb_id2sym(rb_intern2("filename", 8));
558
556
  data_id = rb_id2sym(rb_intern2("data", 4));
559
557
  template_id = rb_id2sym(rb_intern2("template", 8));
@@ -1,3 +1,3 @@
1
1
  module Iodine
2
- VERSION = '0.7.42'.freeze
2
+ VERSION = '0.7.43'.freeze
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: iodine
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.42
4
+ version: 0.7.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Boaz Segev
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-08-02 00:00:00.000000000 Z
11
+ date: 2020-11-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -142,6 +142,7 @@ files:
142
142
  - bin/poc/config.ru
143
143
  - bin/poc/gemfile
144
144
  - bin/poc/www/index.html
145
+ - examples/async_task.ru
145
146
  - examples/config.ru
146
147
  - examples/echo.ru
147
148
  - examples/hello.ru
@@ -243,7 +244,7 @@ licenses:
243
244
  metadata:
244
245
  allowed_push_host: https://rubygems.org
245
246
  post_install_message: |-
246
- Thank you for installing Iodine 0.7.42.
247
+ Thank you for installing Iodine 0.7.43.
247
248
  Remember: if iodine supports your business, it's only fair to give value back (code contributions / donations).
248
249
  rdoc_options: []
249
250
  require_paths: