cent 2.2.0 → 4.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/cent/client.rb CHANGED
@@ -1,273 +1,260 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'faraday'
4
- require 'faraday_middleware' if Gem::Version.new(Faraday::VERSION) <= Gem::Version.new('2.0.0')
5
- require 'cent/http'
4
+ require 'cent/error'
6
5
 
7
6
  module Cent
8
7
  # Cent::Client
9
8
  #
10
- # Main object that handles configuration and requests to centrifugo API
9
+ # Ruby client for Centrifugo server HTTP API (Centrifugo v4+).
11
10
  #
11
+ # Every API method returns the raw parsed response body from Centrifugo —
12
+ # typically `{ "result" => { ... } }` on success. If Centrifugo rejects the
13
+ # request with a top-level `error`, {Cent::ResponseError} is raised with
14
+ # Centrifugo's numeric `code` and `message`. Transport-level problems
15
+ # (network failure, timeout, non-2xx HTTP status, unparseable body) raise
16
+ # other {Cent::Error} subclasses.
17
+ #
18
+ # {#batch} and {#broadcast} are special: their responses contain an array
19
+ # of independent sub-replies, each of which may carry its own `error`
20
+ # field. Those sub-reply errors are NOT raised — callers inspect them
21
+ # manually. See {#batch} for details.
22
+ #
23
+ # @example Basic usage
24
+ # client = Cent::Client.new(api_key: 'secret')
25
+ # client.publish(channel: 'chat', data: { text: 'hi' })
26
+ # # => {"result" => {}}
27
+ #
28
+ # @example Custom Faraday configuration
29
+ # Cent::Client.new(api_key: 'k', endpoint: 'https://c.example.com/api') do |conn|
30
+ # conn.options.open_timeout = 3
31
+ # conn.options.timeout = 7
32
+ # conn.adapter :typhoeus
33
+ # end
12
34
  class Client
13
- # @param endpoint [String]
14
- # (default: 'http://localhost:8000/api') Centrifugo HTTP API URL
15
- #
16
- # @param api_key [String]
17
- # Centrifugo API key(used to perform requests)
18
- #
19
- # @yield [Faraday::Connection] yields connection object so that it can be configured
20
- #
21
- # @example Construct new client instance
22
- # Cent::Client.new(
23
- # endpoint: 'http://localhost:8000/api',
24
- # api_key: 'api key'
25
- # )
26
- #
27
- def initialize(api_key:, endpoint: 'http://localhost:8000/api')
35
+ DEFAULT_ENDPOINT = 'http://localhost:8000/api'
36
+ DEFAULT_TIMEOUT = 10
37
+
38
+ attr_reader :connection
39
+
40
+ # @param api_key [String] Centrifugo HTTP API key (sent as `X-API-Key`).
41
+ # @param endpoint [String] Centrifugo HTTP API base URL.
42
+ # @param timeout [Numeric] Request timeout in seconds.
43
+ # @yield [Faraday::Connection] optional block to further configure the connection.
44
+ def initialize(api_key:, endpoint: DEFAULT_ENDPOINT, timeout: DEFAULT_TIMEOUT, &block)
28
45
  headers = {
29
46
  'Content-Type' => 'application/json',
30
- 'Authorization' => "apikey #{api_key}"
47
+ 'X-API-Key' => api_key
31
48
  }
32
49
 
33
- @connection = Faraday.new(endpoint, headers: headers) do |conn|
34
- conn.request :json # encode req bodies as JSON
50
+ base = endpoint.end_with?('/') ? endpoint : "#{endpoint}/"
35
51
 
36
- conn.response :json # decode response bodies as JSON
52
+ @connection = Faraday.new(base, headers: headers) do |conn|
53
+ conn.options.timeout = timeout
54
+ conn.options.open_timeout = timeout
55
+ conn.request :json
56
+ conn.response :json
37
57
  conn.response :raise_error
38
-
39
- yield conn if block_given?
58
+ block&.call(conn)
40
59
  end
41
60
  end
42
61
 
43
- # Publish data into channel
44
- #
45
- # @param channel [String]
46
- # Name of the channel to publish
47
- #
48
- # @param data [Hash]
49
- # Data for publication in the channel
50
- #
51
- # @example Publish `content: 'hello'` into `chat` channel
52
- # client.publish(channel: 'chat', data: 'hello') #=> {}
53
- #
54
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#publish)
55
- #
56
- # @raise [Cent::Error, Cent::ResponseError]
57
- #
58
- # @return [Hash] Return empty hash in case of successful publish
59
- #
60
- def publish(channel:, data:)
61
- execute('publish', channel: channel, data: data)
62
+ # Publish data into a channel.
63
+ # @see https://centrifugal.dev/docs/server/server_api#publish
64
+ def publish(channel:, data:, skip_history: nil, tags: nil, b64data: nil,
65
+ idempotency_key: nil, delta: nil, version: nil, version_epoch: nil)
66
+ send_command('publish', {
67
+ 'channel' => channel,
68
+ 'data' => data,
69
+ 'skip_history' => skip_history,
70
+ 'tags' => tags,
71
+ 'b64data' => b64data,
72
+ 'idempotency_key' => idempotency_key,
73
+ 'delta' => delta,
74
+ 'version' => version,
75
+ 'version_epoch' => version_epoch
76
+ })
62
77
  end
63
78
 
64
- # Publish data into multiple channels
65
- # (Similar to `#publish` but allows to send the same data into many channels)
66
- #
67
- # @param channels [Array<String>] Collection of channels names to publish
68
- # @param data [Hash] Data for publication in the channels
69
- #
70
- # @example Broadcast `content: 'hello'` into `channel_1`, 'channel_2' channels
71
- # client.broadcast(channels: ['channel_1', 'channel_2'], data: 'hello') #=> {}
72
- #
73
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#broadcast)
74
- #
75
- # @raise [Cent::Error, Cent::ResponseError]
76
- #
77
- # @return [Hash] Return empty hash in case of successful broadcast
78
- #
79
- def broadcast(channels:, data:)
80
- execute('broadcast', channels: channels, data: data)
79
+ # Publish the same data into many channels.
80
+ # @see https://centrifugal.dev/docs/server/server_api#broadcast
81
+ def broadcast(channels:, data:, skip_history: nil, tags: nil, b64data: nil,
82
+ idempotency_key: nil, delta: nil, version: nil, version_epoch: nil)
83
+ send_command('broadcast', {
84
+ 'channels' => channels,
85
+ 'data' => data,
86
+ 'skip_history' => skip_history,
87
+ 'tags' => tags,
88
+ 'b64data' => b64data,
89
+ 'idempotency_key' => idempotency_key,
90
+ 'delta' => delta,
91
+ 'version' => version,
92
+ 'version_epoch' => version_epoch
93
+ })
81
94
  end
82
95
 
83
- # Unsubscribe user from channel
84
- #
85
- # @param channel [String]
86
- # Channel name to unsubscribe from
87
- #
88
- # @param user [String, Integer]
89
- # User ID you want to unsubscribe
90
- #
91
- # @example Unsubscribe user with `id = 1` from `chat` channel
92
- # client.unsubscribe(channel: 'chat', user: '1') #=> {}
93
- #
94
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#unsubscribe)
95
- #
96
- # @raise [Cent::Error, Cent::ResponseError]
97
- #
98
- # @return [Hash] Return empty hash in case of successful unsubscribe
99
- #
100
- def unsubscribe(channel:, user:)
101
- execute('unsubscribe', channel: channel, user: user)
96
+ # Subscribe a user's active session to a channel (server-side subscription).
97
+ # @see https://centrifugal.dev/docs/server/server_api#subscribe
98
+ def subscribe(user:, channel:, info: nil, b64info: nil, client: nil, session: nil,
99
+ data: nil, b64data: nil, recover_since: nil, override: nil)
100
+ send_command('subscribe', {
101
+ 'user' => user,
102
+ 'channel' => channel,
103
+ 'info' => info,
104
+ 'b64info' => b64info,
105
+ 'client' => client,
106
+ 'session' => session,
107
+ 'data' => data,
108
+ 'b64data' => b64data,
109
+ 'recover_since' => recover_since,
110
+ 'override' => override
111
+ })
102
112
  end
103
113
 
104
- # Disconnect user by it's ID
105
- #
106
- # @param user [String, Integer]
107
- # User ID you want to disconnect
108
- #
109
- # @example Disconnect user with `id = 1`
110
- # client.disconnect(user: '1') #=> {}
111
- #
112
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#disconnect)
113
- #
114
- # @raise [Cent::Error, Cent::ResponseError]
115
- #
116
- # @return [Hash] Return empty hash in case of successful disconnect
117
- #
118
- def disconnect(user:)
119
- execute('disconnect', user: user)
114
+ # Unsubscribe a user from a channel.
115
+ # @see https://centrifugal.dev/docs/server/server_api#unsubscribe
116
+ def unsubscribe(user:, channel:, client: nil, session: nil)
117
+ send_command('unsubscribe', {
118
+ 'user' => user,
119
+ 'channel' => channel,
120
+ 'client' => client,
121
+ 'session' => session
122
+ })
120
123
  end
121
124
 
122
- # Get channel presence information
123
- # (all clients currently subscribed on this channel)
124
- #
125
- # @param channel [String] Name of the channel
126
- #
127
- # @example Get presence information for channel `chat`
128
- # client.presence(channel: 'chat') #=> {
129
- # "result" => {
130
- # "presence" => {
131
- # "c54313b2-0442-499a-a70c-051f8588020f" => {
132
- # "client" => "c54313b2-0442-499a-a70c-051f8588020f",
133
- # "user" => "42"
134
- # },
135
- # "adad13b1-0442-499a-a70c-051f858802da" => {
136
- # "client" => "adad13b1-0442-499a-a70c-051f858802da",
137
- # "user" => "42"
138
- # }
139
- # }
140
- # }
141
- # }
142
- #
143
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#presence)
144
- #
145
- # @raise [Cent::Error, Cent::ResponseError]
146
- #
147
- # @return [Hash]
148
- # Return hash with information about all clients currently subscribed on this channel
149
- #
125
+ # Disconnect a user by ID.
126
+ # @see https://centrifugal.dev/docs/server/server_api#disconnect
127
+ def disconnect(user:, client: nil, session: nil, whitelist: nil, disconnect: nil)
128
+ send_command('disconnect', {
129
+ 'user' => user,
130
+ 'client' => client,
131
+ 'session' => session,
132
+ 'whitelist' => whitelist,
133
+ 'disconnect' => disconnect
134
+ })
135
+ end
136
+
137
+ # Refresh a user connection (for unidirectional transports).
138
+ # @see https://centrifugal.dev/docs/server/server_api#refresh
139
+ def refresh(user:, client: nil, session: nil, expired: nil, expire_at: nil)
140
+ send_command('refresh', {
141
+ 'user' => user,
142
+ 'client' => client,
143
+ 'session' => session,
144
+ 'expired' => expired,
145
+ 'expire_at' => expire_at
146
+ })
147
+ end
148
+
149
+ # Get channel presence (all currently subscribed clients).
150
+ # @see https://centrifugal.dev/docs/server/server_api#presence
150
151
  def presence(channel:)
151
- execute('presence', channel: channel)
152
+ send_command('presence', { 'channel' => channel })
152
153
  end
153
154
 
154
- # Get short channel presence information
155
- #
156
- # @param channel [String] Name of the channel
157
- #
158
- # @example Get short presence information for channel `chat`
159
- # client.presence_stats(channel: 'chat') #=> {
160
- # "result" => {
161
- # "num_clients" => 0,
162
- # "num_users" => 0
163
- # }
164
- # }
165
- #
166
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#presence_stats)
167
- #
168
- # @raise [Cent::Error, Cent::ResponseError]
169
- #
170
- # @return [Hash]
171
- # Return hash with short presence information about channel
172
- #
155
+ # Get short presence stats for a channel.
156
+ # @see https://centrifugal.dev/docs/server/server_api#presence_stats
173
157
  def presence_stats(channel:)
174
- execute('presence_stats', channel: channel)
158
+ send_command('presence_stats', { 'channel' => channel })
175
159
  end
176
160
 
177
- # Get channel history information
178
- # (list of last messages published into channel)
179
- #
180
- # @param channel [String] Name of the channel
181
- #
182
- # @example Get history for channel `chat`
183
- # client.history(channel: 'chat') #=> {
184
- # "result" => {
185
- # "publications" => [
186
- # {
187
- # "data" => {
188
- # "text" => "hello"
189
- # },
190
- # "uid" => "BWcn14OTBrqUhTXyjNg0fg"
191
- # },
192
- # {
193
- # "data" => {
194
- # "text" => "hi!"
195
- # },
196
- # "uid" => "Ascn14OTBrq14OXyjNg0hg"
197
- # }
198
- # ]
199
- # }
200
- # }
201
- #
202
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#history)
203
- #
204
- # @raise [Cent::Error, Cent::ResponseError]
205
- #
206
- # @return [Hash]
207
- # Return hash with a list of last messages published into channel
208
- #
209
- def history(channel:)
210
- execute('history', channel: channel)
161
+ # Get channel history (recent publications).
162
+ # @see https://centrifugal.dev/docs/server/server_api#history
163
+ def history(channel:, limit: nil, since: nil, reverse: nil)
164
+ send_command('history', {
165
+ 'channel' => channel,
166
+ 'limit' => limit,
167
+ 'since' => since,
168
+ 'reverse' => reverse
169
+ })
211
170
  end
212
171
 
213
- # Get list of active(with one or more subscribers) channels.
214
- #
215
- # @example Get active channels list
216
- # client.channels #=> {
217
- # "result" => {
218
- # "channels" => [
219
- # "chat"
220
- # ]
221
- # }
222
- # }
223
- #
224
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#channels)
225
- #
226
- # @raise [Cent::Error, Cent::ResponseError]
227
- #
228
- # @return [Hash]
229
- # Return hash with a list of active channels
230
- #
231
- def channels
232
- execute('channels', {})
172
+ # Remove all publications from a channel's history.
173
+ # @see https://centrifugal.dev/docs/server/server_api#history_remove
174
+ def history_remove(channel:)
175
+ send_command('history_remove', { 'channel' => channel })
233
176
  end
234
177
 
235
- # Get information about running Centrifugo nodes
236
- #
237
- # @example Get running centrifugo nodes list
238
- # client.info #=> {
239
- # "result" => {
240
- # "nodes" => [
241
- # {
242
- # "name" => "Alexanders-MacBook-Pro.local_8000",
243
- # "num_channels" => 0,
244
- # "num_clients" => 0,
245
- # "num_users" => 0,
246
- # "uid" => "f844a2ed-5edf-4815-b83c-271974003db9",
247
- # "uptime" => 0,
248
- # "version" => ""
249
- # }
250
- # ]
251
- # }
252
- # }
253
- #
254
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#info)
255
- #
256
- # @raise [Cent::Error, Cent::ResponseError]
257
- #
258
- # @return [Hash]
259
- # Return hash with a list of last messages published into channel
260
- #
178
+ # List active channels (channels with at least one subscriber).
179
+ # @see https://centrifugal.dev/docs/server/server_api#channels
180
+ def channels(pattern: nil)
181
+ send_command('channels', { 'pattern' => pattern })
182
+ end
183
+
184
+ # Get information about running Centrifugo nodes.
185
+ # @see https://centrifugal.dev/docs/server/server_api#info
261
186
  def info
262
- execute('info', {})
187
+ send_command('info', {})
188
+ end
189
+
190
+ # Send many commands in a single request.
191
+ #
192
+ # The response is shaped `{ "replies" => [<reply>, ...] }` — note there
193
+ # is no top-level `result` wrapper, unlike every other method. Each reply
194
+ # in the array corresponds to one command (in the order they were sent
195
+ # when `parallel` is not set) and has the shape `{ "<method>" => <result> }`
196
+ # on success or `{ "error" => { "code" => ..., "message" => ... } }` on a
197
+ # per-command failure.
198
+ #
199
+ # These per-command errors are **not** raised as {Cent::ResponseError} —
200
+ # that would make partial-failure responses impossible to inspect. The
201
+ # caller is expected to walk `response["replies"]` and check each entry.
202
+ # If Centrifugo rejects the batch request as a whole (e.g. malformed
203
+ # top-level body), the top-level `error` field is present and
204
+ # {Cent::ResponseError} is raised normally.
205
+ #
206
+ # @example
207
+ # response = client.batch(commands: [
208
+ # { 'publish' => { 'channel' => 'a', 'data' => {} } },
209
+ # { 'publish' => { 'channel' => 'unknown:b', 'data' => {} } }
210
+ # ])
211
+ # response['replies'].each do |reply|
212
+ # if reply['error']
213
+ # warn "command failed: #{reply['error']['code']} #{reply['error']['message']}"
214
+ # end
215
+ # end
216
+ #
217
+ # @param commands [Array<Hash>] Each element is a command object of the
218
+ # form `{ "publish" => { ... } }`, `{ "broadcast" => { ... } }`, etc.
219
+ # @param parallel [Boolean, nil] When true, Centrifugo processes commands
220
+ # in parallel (lower latency, order not guaranteed).
221
+ # @see https://centrifugal.dev/docs/server/server_api#batch
222
+ def batch(commands:, parallel: nil)
223
+ send_command('batch', {
224
+ 'commands' => commands,
225
+ 'parallel' => parallel
226
+ })
263
227
  end
264
228
 
265
229
  private
266
230
 
267
- def execute(method, data)
268
- body = { method: method, params: data }
231
+ def send_command(method, params)
232
+ body = connection.post(method, params.compact).body
233
+ check_response_error!(body)
234
+ body
235
+ rescue Faraday::TimeoutError => e
236
+ raise Cent::TimeoutError, e.message
237
+ rescue Faraday::ConnectionFailed => e
238
+ raise Cent::NetworkError, e.message
239
+ rescue Faraday::UnauthorizedError => e
240
+ raise Cent::UnauthorizedError.new(status: 401, message: e.message)
241
+ rescue Faraday::ClientError, Faraday::ServerError => e
242
+ status = e.response_status || e.response&.dig(:status)
243
+ raise Cent::TransportError.new(status: status, message: e.message)
244
+ rescue Faraday::ParsingError => e
245
+ raise Cent::DecodeError, e.message
246
+ end
247
+
248
+ # Top-level `error` means Centrifugo rejected the whole request. Batch
249
+ # and broadcast sub-reply errors live inside arrays and are NOT raised —
250
+ # callers inspect them manually (see #batch docs).
251
+ def check_response_error!(body)
252
+ return unless body.is_a?(Hash) && body['error'].is_a?(Hash)
269
253
 
270
- Cent::HTTP.new(connection: @connection).post(body: body)
254
+ raise Cent::ResponseError.new(
255
+ code: body['error']['code'],
256
+ message: body['error']['message']
257
+ )
271
258
  end
272
259
  end
273
260
  end
data/lib/cent/error.rb CHANGED
@@ -1,9 +1,46 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Cent
4
- # Cent::Error
5
- #
6
- # Wrapper for all errors(failures).
7
- #
4
+ # Base class for all errors raised by this library.
8
5
  class Error < StandardError; end
6
+
7
+ # Raised when Centrifugo is unreachable (DNS failure, connection refused, ...).
8
+ class NetworkError < Error; end
9
+
10
+ # Raised when the HTTP request times out.
11
+ class TimeoutError < Error; end
12
+
13
+ # Raised when Centrifugo returns a non-2xx HTTP status.
14
+ class TransportError < Error
15
+ attr_reader :status
16
+
17
+ def initialize(status:, message: nil)
18
+ @status = status
19
+ super(message || "HTTP #{status}")
20
+ end
21
+ end
22
+
23
+ # Raised when Centrifugo returns HTTP 401 (invalid API key).
24
+ class UnauthorizedError < TransportError; end
25
+
26
+ # Raised when response from Centrifugo cannot be decoded (not valid JSON).
27
+ class DecodeError < Error; end
28
+
29
+ # Raised when Centrifugo returns a top-level `error` in the response body
30
+ # (API-level failure, e.g., unknown channel, namespace not found). Exposes
31
+ # Centrifugo's numeric `code` and human-readable `message`. See
32
+ # https://centrifugal.dev/docs/server/server_api#error for the full list
33
+ # of codes.
34
+ #
35
+ # Note: for `batch` and `broadcast`, individual sub-reply errors are NOT
36
+ # raised — those responses contain an array of independent replies, and
37
+ # each entry should be inspected by the caller for its own `error` key.
38
+ class ResponseError < Error
39
+ attr_reader :code
40
+
41
+ def initialize(code:, message:)
42
+ @code = code
43
+ super(message)
44
+ end
45
+ end
9
46
  end