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 +4 -4
- data/CHANGELOG.md +7 -1
- data/README.md +2 -2
- data/SPEC-PubSub-Draft.md +89 -47
- data/SPEC-WebSocket-Draft.md +92 -55
- data/examples/async_task.ru +92 -0
- data/ext/iodine/iodine.c +2 -0
- data/ext/iodine/iodine.h +1 -0
- data/ext/iodine/iodine_caller.c +1 -1
- data/ext/iodine/iodine_connection.c +20 -8
- data/ext/iodine/iodine_http.c +4 -6
- data/ext/iodine/iodine_mustache.c +2 -4
- data/lib/iodine/version.rb +1 -1
- metadata +4 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7a444a825ab9cf0e6bd11fe14fb20c6a777e9282c1bea56cf5dc869087ce7a44
|
4
|
+
data.tar.gz: 4c698a81975a588e8a5c185d57f9ca76ae1d37200d2a258f5d38631de386e9dd
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9659c57e4d0c9f884b52dc07cf7b4c1a707f78d27bf03d058db1c2bfe2c873141a7d128c56b933098fa61a1f8fc48cce1319fea298c15bc8c606bb665b6d7d88
|
7
|
+
data.tar.gz: f183fb08856e81963d90d03ba0319f2633a72634beb8b37b2a5cee6c66dfa61710de5532abb5c82dd059d0c8382b9968a824f2caefeee2a53213e2bab2bac0b3
|
data/CHANGELOG.md
CHANGED
@@ -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
|
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
|
-
|
580
|
-
|
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).
|
data/SPEC-PubSub-Draft.md
CHANGED
@@ -1,66 +1,106 @@
|
|
1
|
-
# Ruby
|
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
|
-
|
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
|
13
|
+
The purpose of this specification is:
|
8
14
|
|
9
|
-
|
15
|
+
1. To keep separation of concerns by avoiding inter-process-communication logic (IPC) in the application code base.
|
10
16
|
|
11
|
-
This
|
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
|
-
|
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
|
-
|
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
|
-
|
23
|
+
3. To provide common considerations and guidelines for pub/sub implementors to consider when implementing their pub/sub modules.
|
18
24
|
|
19
|
-
|
25
|
+
Some concerns are common for pub/sub implementors, such as integrating third party message brokers (Redis, RabbitMQ, Cassandra)
|
20
26
|
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
33
|
+
* `subscribe(to, is_pattern = false) { |from, message| optional_block }` where:
|
28
34
|
|
29
|
-
*
|
35
|
+
* `to` is a named **channel** (or **pattern**).
|
30
36
|
|
31
|
-
|
37
|
+
The implementation **MAY** support pattern matching for named channels (`to`). The pattern matching algorithm used, if any, **SHOULD** be documented.
|
32
38
|
|
33
|
-
|
39
|
+
If the implementation does **NOT** support pattern matching and `is_pattern` is truthful, the implementation **MUST** raise and exception.
|
34
40
|
|
35
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
65
|
+
* `to` is a named channel, same as detailed in `subscribe`.
|
46
66
|
|
47
|
-
|
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
|
-
* `
|
69
|
+
* `message` a String containing the data to be published.
|
50
70
|
|
51
|
-
|
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
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
115
|
+
The implementation **MUST** remove the engine from the attached engine list. The opposite of `attach`.
|
76
116
|
|
77
|
-
* `default
|
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
|
-
|
127
|
+
A `PubSubEngine` instance object **MUST** implement the following methods:
|
88
128
|
|
89
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
135
|
+
This method **MUST NOT** raise an exception.
|
96
136
|
|
97
|
-
|
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
|
-
|
139
|
+
The method's semantics are similar to `subscribe` only is performs the opposite action.
|
100
140
|
|
101
|
-
|
141
|
+
This method **MUST NOT** raise an exception.
|
102
142
|
|
103
|
-
The method
|
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
|
-
|
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
|
-
|
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
|
-
|
155
|
+
i.e., if the implementation's global `publish` method is in a class called `Iodine`:
|
114
156
|
|
115
157
|
```ruby
|
116
|
-
|
158
|
+
Iodine.publish channel, message, false
|
117
159
|
```
|
data/SPEC-WebSocket-Draft.md
CHANGED
@@ -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
|
8
|
+
This document details a Rack specification extension for WebSocket / EventSource servers.
|
9
9
|
|
10
|
-
The purpose of
|
10
|
+
The purpose of this specification is:
|
11
11
|
|
12
|
-
1. To
|
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
|
-
|
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
|
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,
|
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
|
-
|
38
|
+
* The server **SHOULD** ignore the Callback Object and process the response as if it did not exist.
|
31
39
|
|
32
|
-
|
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)`
|
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)`
|
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`
|
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
|
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
|
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
|
-
* `
|
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
|
-
|
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
|
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
|
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?`
|
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`
|
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
|
-
|
105
|
+
* `class` **MUST** return the client's Class, allowing it be extended with additional features (such as Pub/Sub, etc').
|
86
106
|
|
87
|
-
|
107
|
+
**Note**: Ruby adds this method automatically to every class, no need to do a thing.
|
88
108
|
|
89
|
-
|
109
|
+
The server **MAY** support the following (optional) methods for the `client` object:
|
90
110
|
|
91
|
-
|
111
|
+
* `handler` if implemented, **MUST** return the callback object linked to the `client` object.
|
92
112
|
|
93
|
-
|
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
|
-
|
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.
|
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
|
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
|
-
|
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
|
-
|
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
|
-
|
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`
|
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
|
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
|
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
|
-
|
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 "
|
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
|
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
|
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
|
-
|
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", "#{
|
213
|
+
client.publish "chat", "#{env[:nickname]} joined the chat."
|
178
214
|
end
|
179
215
|
def on_message(client, data)
|
180
|
-
client.publish "chat", "#{
|
216
|
+
client.publish "chat", "#{env[:nickname]}: #{data}"
|
181
217
|
end
|
182
218
|
def on_close(client)
|
183
|
-
client.publish "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[
|
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
|
data/ext/iodine/iodine.c
CHANGED
@@ -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);
|
data/ext/iodine/iodine.h
CHANGED
@@ -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)})
|
data/ext/iodine/iodine_caller.c
CHANGED
@@ -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);
|
data/ext/iodine/iodine_http.c
CHANGED
@@ -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,
|
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,
|
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,
|
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,
|
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));
|
data/lib/iodine/version.rb
CHANGED
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.
|
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-
|
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.
|
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:
|