cent 3.0.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,272 +1,260 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'faraday'
4
- require 'cent/http'
4
+ require 'cent/error'
5
5
 
6
6
  module Cent
7
7
  # Cent::Client
8
8
  #
9
- # Main object that handles configuration and requests to centrifugo API
9
+ # Ruby client for Centrifugo server HTTP API (Centrifugo v4+).
10
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
11
34
  class Client
12
- # @param endpoint [String]
13
- # (default: 'http://localhost:8000/api') Centrifugo HTTP API URL
14
- #
15
- # @param api_key [String]
16
- # Centrifugo API key(used to perform requests)
17
- #
18
- # @yield [Faraday::Connection] yields connection object so that it can be configured
19
- #
20
- # @example Construct new client instance
21
- # Cent::Client.new(
22
- # endpoint: 'http://localhost:8000/api',
23
- # api_key: 'api key'
24
- # )
25
- #
26
- 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)
27
45
  headers = {
28
46
  'Content-Type' => 'application/json',
29
- 'Authorization' => "apikey #{api_key}"
47
+ 'X-API-Key' => api_key
30
48
  }
31
49
 
32
- @connection = Faraday.new(endpoint, headers: headers) do |conn|
33
- conn.request :json # encode req bodies as JSON
50
+ base = endpoint.end_with?('/') ? endpoint : "#{endpoint}/"
34
51
 
35
- 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
36
57
  conn.response :raise_error
37
-
38
- yield conn if block_given?
58
+ block&.call(conn)
39
59
  end
40
60
  end
41
61
 
42
- # Publish data into channel
43
- #
44
- # @param channel [String]
45
- # Name of the channel to publish
46
- #
47
- # @param data [Hash]
48
- # Data for publication in the channel
49
- #
50
- # @example Publish `content: 'hello'` into `chat` channel
51
- # client.publish(channel: 'chat', data: 'hello') #=> {}
52
- #
53
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#publish)
54
- #
55
- # @raise [Cent::Error, Cent::ResponseError]
56
- #
57
- # @return [Hash] Return empty hash in case of successful publish
58
- #
59
- def publish(channel:, data:)
60
- 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
+ })
61
77
  end
62
78
 
63
- # Publish data into multiple channels
64
- # (Similar to `#publish` but allows to send the same data into many channels)
65
- #
66
- # @param channels [Array<String>] Collection of channels names to publish
67
- # @param data [Hash] Data for publication in the channels
68
- #
69
- # @example Broadcast `content: 'hello'` into `channel_1`, 'channel_2' channels
70
- # client.broadcast(channels: ['channel_1', 'channel_2'], data: 'hello') #=> {}
71
- #
72
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#broadcast)
73
- #
74
- # @raise [Cent::Error, Cent::ResponseError]
75
- #
76
- # @return [Hash] Return empty hash in case of successful broadcast
77
- #
78
- def broadcast(channels:, data:)
79
- 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
+ })
80
94
  end
81
95
 
82
- # Unsubscribe user from channel
83
- #
84
- # @param channel [String]
85
- # Channel name to unsubscribe from
86
- #
87
- # @param user [String, Integer]
88
- # User ID you want to unsubscribe
89
- #
90
- # @example Unsubscribe user with `id = 1` from `chat` channel
91
- # client.unsubscribe(channel: 'chat', user: '1') #=> {}
92
- #
93
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#unsubscribe)
94
- #
95
- # @raise [Cent::Error, Cent::ResponseError]
96
- #
97
- # @return [Hash] Return empty hash in case of successful unsubscribe
98
- #
99
- def unsubscribe(channel:, user:)
100
- 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
+ })
101
112
  end
102
113
 
103
- # Disconnect user by it's ID
104
- #
105
- # @param user [String, Integer]
106
- # User ID you want to disconnect
107
- #
108
- # @example Disconnect user with `id = 1`
109
- # client.disconnect(user: '1') #=> {}
110
- #
111
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#disconnect)
112
- #
113
- # @raise [Cent::Error, Cent::ResponseError]
114
- #
115
- # @return [Hash] Return empty hash in case of successful disconnect
116
- #
117
- def disconnect(user:)
118
- 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
+ })
119
123
  end
120
124
 
121
- # Get channel presence information
122
- # (all clients currently subscribed on this channel)
123
- #
124
- # @param channel [String] Name of the channel
125
- #
126
- # @example Get presence information for channel `chat`
127
- # client.presence(channel: 'chat') #=> {
128
- # "result" => {
129
- # "presence" => {
130
- # "c54313b2-0442-499a-a70c-051f8588020f" => {
131
- # "client" => "c54313b2-0442-499a-a70c-051f8588020f",
132
- # "user" => "42"
133
- # },
134
- # "adad13b1-0442-499a-a70c-051f858802da" => {
135
- # "client" => "adad13b1-0442-499a-a70c-051f858802da",
136
- # "user" => "42"
137
- # }
138
- # }
139
- # }
140
- # }
141
- #
142
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#presence)
143
- #
144
- # @raise [Cent::Error, Cent::ResponseError]
145
- #
146
- # @return [Hash]
147
- # Return hash with information about all clients currently subscribed on this channel
148
- #
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
149
151
  def presence(channel:)
150
- execute('presence', channel: channel)
152
+ send_command('presence', { 'channel' => channel })
151
153
  end
152
154
 
153
- # Get short channel presence information
154
- #
155
- # @param channel [String] Name of the channel
156
- #
157
- # @example Get short presence information for channel `chat`
158
- # client.presence_stats(channel: 'chat') #=> {
159
- # "result" => {
160
- # "num_clients" => 0,
161
- # "num_users" => 0
162
- # }
163
- # }
164
- #
165
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#presence_stats)
166
- #
167
- # @raise [Cent::Error, Cent::ResponseError]
168
- #
169
- # @return [Hash]
170
- # Return hash with short presence information about channel
171
- #
155
+ # Get short presence stats for a channel.
156
+ # @see https://centrifugal.dev/docs/server/server_api#presence_stats
172
157
  def presence_stats(channel:)
173
- execute('presence_stats', channel: channel)
158
+ send_command('presence_stats', { 'channel' => channel })
174
159
  end
175
160
 
176
- # Get channel history information
177
- # (list of last messages published into channel)
178
- #
179
- # @param channel [String] Name of the channel
180
- #
181
- # @example Get history for channel `chat`
182
- # client.history(channel: 'chat') #=> {
183
- # "result" => {
184
- # "publications" => [
185
- # {
186
- # "data" => {
187
- # "text" => "hello"
188
- # },
189
- # "uid" => "BWcn14OTBrqUhTXyjNg0fg"
190
- # },
191
- # {
192
- # "data" => {
193
- # "text" => "hi!"
194
- # },
195
- # "uid" => "Ascn14OTBrq14OXyjNg0hg"
196
- # }
197
- # ]
198
- # }
199
- # }
200
- #
201
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#history)
202
- #
203
- # @raise [Cent::Error, Cent::ResponseError]
204
- #
205
- # @return [Hash]
206
- # Return hash with a list of last messages published into channel
207
- #
208
- def history(channel:)
209
- 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
+ })
210
170
  end
211
171
 
212
- # Get list of active(with one or more subscribers) channels.
213
- #
214
- # @example Get active channels list
215
- # client.channels #=> {
216
- # "result" => {
217
- # "channels" => [
218
- # "chat"
219
- # ]
220
- # }
221
- # }
222
- #
223
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#channels)
224
- #
225
- # @raise [Cent::Error, Cent::ResponseError]
226
- #
227
- # @return [Hash]
228
- # Return hash with a list of active channels
229
- #
230
- def channels
231
- 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 })
232
176
  end
233
177
 
234
- # Get information about running Centrifugo nodes
235
- #
236
- # @example Get running centrifugo nodes list
237
- # client.info #=> {
238
- # "result" => {
239
- # "nodes" => [
240
- # {
241
- # "name" => "Alexanders-MacBook-Pro.local_8000",
242
- # "num_channels" => 0,
243
- # "num_clients" => 0,
244
- # "num_users" => 0,
245
- # "uid" => "f844a2ed-5edf-4815-b83c-271974003db9",
246
- # "uptime" => 0,
247
- # "version" => ""
248
- # }
249
- # ]
250
- # }
251
- # }
252
- #
253
- # @see (https://centrifugal.github.io/centrifugo/server/http_api/#info)
254
- #
255
- # @raise [Cent::Error, Cent::ResponseError]
256
- #
257
- # @return [Hash]
258
- # Return hash with a list of last messages published into channel
259
- #
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
260
186
  def info
261
- 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
+ })
262
227
  end
263
228
 
264
229
  private
265
230
 
266
- def execute(method, data)
267
- 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)
268
253
 
269
- Cent::HTTP.new(connection: @connection).post(body: body)
254
+ raise Cent::ResponseError.new(
255
+ code: body['error']['code'],
256
+ message: body['error']['message']
257
+ )
270
258
  end
271
259
  end
272
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