cent 3.0.0 → 5.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9fadf11a9b2cdfa2b9c490e59adbf052515c9a0c4f5091405ffd3bca4f345bfb
4
- data.tar.gz: 07e0c29ede9d249c0d8b36d3d432a37fa8048c82e54c7540b8ef67131b103d76
3
+ metadata.gz: ad578d47b0681afc90300faa480857bdb5dd94bb7e7e3462aec750da5ea1a8b6
4
+ data.tar.gz: 158a26a1c14e98e31e43c288969dc1686dd461bdbaccdcf7df6855b73093760b
5
5
  SHA512:
6
- metadata.gz: f99f6ab6178fda5cce7adcac441e35313c2c2990fcfedbf685ddbfd4fc2c70bcd1098231c3ee1580b39154cca836ef590d05a8ac5032e8ca8946e16666ab6716
7
- data.tar.gz: 6158ba6dc6ccd667b206f2d50ce7c7a832412bf5360686de0f0c8948df219c8a84d5d273a31fcaeb819e4474580a554eb40545e5512628a2eee445c7e5cba9b9
6
+ metadata.gz: 46b906c03002c52bda6bd4f5d6ccb082d05b23927b108030a175f6cd0782fc7e82bd2d93cb6494c457966a45736f245ca32136ffd413e073d773e4baf292d7cc
7
+ data.tar.gz: 6bbcee3e1b4407d69f15ebfa660e41dcc980c2ed3b02895d628cb743159e568638d856915004d046074b6ba9612ae517de8f9fdb8e1676e0ea4055115567f445
data/CHANGELOG.md CHANGED
@@ -1,3 +1,96 @@
1
+ v5.0.0
2
+ ======
3
+
4
+ cent 5 is a rewrite, to the [Centrifugo server SDK specification](https://github.com/centrifugal/gocent/blob/master/spec.md), version 0.4, which gocent and the other Centrifugo server SDKs follow too. It needs Ruby 3.4 or newer and supports Centrifugo v6 - OSS and PRO; the map and shared poll methods need v6.8.0 or newer, `ApiError#temporary?` v6.10.0 or newer. The json gem is its only dependency: Faraday and jwt are gone. Migrating from cent 4: see [MIGRATING.md](https://github.com/centrifugal/rubycent/blob/master/MIGRATING.md); an application which needs time to move can stay on `gem "cent", "~> 4.0"`.
5
+
6
+ Generated from Centrifugo's protos at [e21aeb50](https://github.com/centrifugal/centrifugo/commit/e21aeb50ab6fc4d525fc817473574f728da6c3ad) on its master, after v6.10.2: the same messages as v6.10.2, with the comments the constants are generated from, and `ConnectResult.profile` for the connect proxy, which Centrifugo PRO newer than v6.10.2 reads.
7
+
8
+ **Client and calls**
9
+
10
+ * A method for every API method, generated from Centrifugo's `api.proto` with its documentation - the map, shared poll and Centrifugo PRO methods included - taking its request's fields as keyword arguments: `client.publish(channel: "news", data: ...)`. A nested message is its class or a Hash of its fields; an unknown keyword or field is refused, with a suggestion.
11
+ * Results are `Data` objects - `res.offset`, not `res["result"]["offset"]` - with `to_h`, `as_json` and `to_json` giving wire names, nested results too: `render json: result` in Rails.
12
+ * Fields the protos mark required are required keywords, of the methods and of the nested messages' classes - `Cent::RateLimitPolicy.new(rate:, interval_ms:)` - and are checked for a value before sending: given `nil`, `""`, `0`, `[]` or a message with nothing set, a call raises `InvalidRequestError` naming the field's path, `limit_strategy.rate_limit.policies[0].rate is required`.
13
+ * A field at its zero value is not sent, nor a nested message with nothing set - but for `device_update`'s `user_update`, `timezone_update`, `locale_update` and `meta_update`, sent whenever given, even empty: `timezone_update: { timezone: "" }` clears the timezone, `user_update: { user: "" }` detaches the devices from their user. `BoolValue` fields are `true`, `false` or nil, which leaves the server's setting alone.
14
+ * The client is created once and shared: safe for concurrent use from threads, and under a fiber scheduler (Async, Falcon) calls wait without blocking other fibers. A client used in a forked process - Puma's `preload_app!`, a Sidekiq swarm - starts its own connections and batch threads there.
15
+ * Its configuration is checked when it is created, `InvalidConfigError` naming the option and the fix: `api_endpoint:` (required, no default) or `api_endpoint_func:`, `api_key:` (nil for none) or `bearer_token_func:`, `headers:`, `request_timeout:`, `http_options:`, `http_client:`, `auto_batch:`, `on_request:`, `batch_context:`. `inspect` hides credentials.
16
+ * An endpoint from `api_endpoint_func`, or a token from `bearer_token_func`, which cannot be sent fails the call with `InvalidConfigError` before sending. An exception of either function's own fails the call with `Cent::FunctionError`: its `function` is the option's name, its `cause` the exception, and `Cent.retryable?` judges it by that cause.
17
+ * Net::HTTP connections kept alive in a pool, up to 256 to a host, idle ones closed after 50 seconds. Redirects are not followed: a redirect is an `HTTPError` hinting to check `api_endpoint`'s scheme and path. `http_options:` for TLS settings, `http_client:` for another HTTP library or a fake in tests.
18
+ * `close` refuses new calls with `ClosedError` and waits for those in progress, automatic batches included; with `timeout:` it raises `TimeoutError` when they have not finished, leaving them to go on. It may be called again, concurrently too, each call waiting alike; `broadcast_with_retry` stops at its next attempt with `ClosedError`.
19
+
20
+ **Timeouts**
21
+
22
+ * `request_timeout` - 10 seconds, also when nil - bounds every call: connecting and TLS, sending, and the reply, however slowly it comes. A watchdog thread finishes a connection at its call's deadline through Net::HTTP's public `finish`, so a reply's headers trickling in no longer outlast it; it starts afresh in a forked process. However a call times out, it raises `Cent::TimeoutError`.
23
+ * A call's `timeout:` replaces `request_timeout` for that call, longer or shorter. Ruby has no deadline a call runs under for cent to read: a `Timeout.timeout` around a call only cuts it short.
24
+ * A timeout cannot be turned off: `request_timeout: 0` is refused with `InvalidConfigError`, and a call's `timeout: 0` with `InvalidRequestError`, as negative values are.
25
+
26
+ **Errors**
27
+
28
+ * `Cent::ApiError` with a subclass per known code - `UnknownChannelError`, `NotFoundError`, `BadRequestError`, `NotAvailableError`, `TooManyRequestsError`, `UnrecoverablePositionError`, `ConflictError`, `InternalError` - and `code`, `server_message`, `temporary?`; an unknown code is an `ApiError` itself.
29
+ * `HTTPError` (`status_code`, `body`, `headers`) and its `UnauthorizedError` for rejected credentials, `DecodeError`, `InvalidRequestError`, `InvalidConfigError`, `FunctionError`, `NetworkError`, `TimeoutError`, `ClosedError`, `BatchSentError`, `BatchNotSentError`: all `Cent::Error`. Messages carry hints: a 404 says whether the endpoint lacks the API prefix or the server is Centrifugo OSS answering a PRO method.
30
+ * Centrifugo's transport error mode gives the same errors as a 200 reply.
31
+
32
+ **Retries and idempotency**
33
+
34
+ * `Cent.retryable?(err)` says whether a new attempt may succeed; `Cent.exponential_backoff(base, limit)` for waits.
35
+ * `broadcast_with_retry` retries only the channels which failed for a temporary reason, with one `idempotency_key` on every attempt - made for the call when none is given - up to `max_attempts:`. `timeout:` bounds the whole call, waits included; `attempt_timeout:` is each attempt's own timeout, replacing `request_timeout`, the call's `timeout:` still ending an attempt sooner. Its block sees each failed attempt, returns the wait, and raises to stop.
36
+ * The docs say how to choose keys: one per publication, reused only to retry it, naming the event; the memory, Redis and PostgreSQL brokers keep keys, the NATS broker ignores them.
37
+
38
+ **Broadcast and batch**
39
+
40
+ * **Breaking:** a broadcast or batch in which some parts failed raises `BroadcastFailedChannelsError` or `BatchFailedCommandsError`, whose `result` holds every part's outcome and whose `failed` lists the failed parts, also when every part failed - cent 4 returned the response with the errors inside. Rescue them for best-effort sends: see [MIGRATING.md](https://github.com/centrifugal/rubycent/blob/master/MIGRATING.md#broadcasts-and-batches-now-raise). A broadcast or batch failing as a whole raises its own error instead, and every pending command of a batch returns that error.
41
+ * `client.new_batch` has the client's methods, each adding a command and returning a `Cent::Pending` whose `value` is its result; `batch.send`, once; `client.new_batch(parallel: true)` for Centrifugo to run the commands concurrently. A command the client can tell is invalid fails the batch before sending, naming its index.
42
+
43
+ **Automatic batching**
44
+
45
+ * `auto_batch: Cent::AutoBatch.new(max_in_flight:, max_batch_size:)`: below `max_in_flight` concurrent requests every call goes alone, at once; at the cap, calls queue and go out together in one batch request as one returns. Every call keeps its own result, error and timeout; a batch lasts until the latest of its calls' deadlines, at least `request_timeout`. `batch_context:` carries values such as a tracing span into the batch's thread.
46
+
47
+ **Pages and positions**
48
+
49
+ * `history_all`, `device_list_all`, `device_topic_list_all`, `user_topic_list_all` and `map_read_state_all` read page after page, with a block or as an `Enumerator` reading a page when the iteration reaches it. `page_timeout:` bounds each page's request, replacing `request_timeout` for it; an iteration lasts as long as the caller's loop.
50
+ * `history_all` yields each publication as a `Cent::HistoryItem`, with its `position`; it needs a `limit` of 1 or more and reads forward only. `map_read_state_all` yields each entry as a `Cent::MapStateItem`, with the state's `position`, and reads every page after the first at the first one's revision, so that a map reset between pages raises `UnrecoverablePositionError` instead of mixing two states.
51
+ * `position`, a `Cent::StreamPosition`, on the results of `publish`, `history`, `map_publish`, `map_remove`, `map_read_state` and `map_read_stream`: what a publish returned is the `since:` of the history call reading what came after it.
52
+
53
+ **Constants**
54
+
55
+ * A constant per value of the fields with a fixed set of values, named as the specification pins them for every SDK: `Cent::PROVIDER_FCM`, `Cent::PLATFORM_IOS`, `Cent::OP_ADD`, `Cent::STATUS_DELIVERED`, `Cent::KEY_MODE_IF_NEW`, `Cent::FILTER_NODE_OP_AND`, `Cent::CMP_EQ`, and in `Cent::Proxy` `PROTOCOL_JSON`, `ENCODING_BINARY`, `ALLOW_SUB`, `CAPABILITY_MATCH_WILDCARD`, `CHANNEL_EVENT_TYPE_OCCUPIED` and the rest. The fields stay strings: a value Centrifugo adds can be sent before cent names it.
56
+
57
+ **Payloads**
58
+
59
+ * `data`, `info`, `meta` and the other payloads are Ruby values `JSON.generate` writes, or a `JSON::Fragment` sent as its text; a payload from a reply sent back unchanged is the same JSON value. NaN, infinities, binary strings, invalid UTF-8 and values JSON cannot hold are refused before sending, naming where they are. nil is no payload: JSON null is `JSON::Fragment.new("null")`.
60
+
61
+ **Tokens and JWKS**
62
+
63
+ * `Cent::Token.connection` and `Cent::Token.subscription` with every claim Centrifugo reads - `profile:` included, the compression profile of Centrifugo PRO - signed with `Cent::Token::Key.hmac`, `.rsa`, `.ecdsa`, `.ed25519` or `.from_pem` (HS256, RS256, ES256/384/512, EdDSA) from Ruby's OpenSSL. RSA keys are of 2048 bits or more, ECDSA keys on P-256, P-384 or P-521: others are refused with `Cent::Token::Error` when the key is made. An expiry is required: a `Time`, or `:never` chosen explicitly.
64
+ * `Cent::Token.jwks(*keys)` and the `Cent::Endpoints::JWKS` Rack app serve the public keys for Centrifugo's [`client.token.jwks_public_endpoint`](https://centrifugal.dev/docs/server/authentication), for key rotation without a restart.
65
+
66
+ **API tokens (Centrifugo PRO)**
67
+
68
+ * `Cent::Token.api` signs server API tokens; `Cent::Token::ApiTokenSource` issues them for `bearer_token_func:`, reusing each for half its lifetime, its key fixed or from `current_key:` as keys rotate.
69
+
70
+ **Endpoints for client SDKs**
71
+
72
+ * `Cent::Endpoints::ConnectionToken`, `SubscriptionToken` and `SharedPollSignature`: the endpoints the `getToken` and `getSignature` callbacks of client SDKs call, as Rack apps with a framework-free core. Raising `Cent::Endpoints::ForbiddenError` answers 403, on which client SDKs stop asking - they report it as their `UnauthorizedError`; it is not `Cent::UnauthorizedError`, the API refusing your credentials.
73
+
74
+ **Shared poll**
75
+
76
+ * `Cent::SharedPoll.sign` for shared poll signatures, its expiry required: a `Time`, or `:never`.
77
+
78
+ **Proxy handlers**
79
+
80
+ * `Cent::Proxy`: a handler for every proxy type - `ConnectHandler`, `RefreshHandler`, `SubscribeHandler`, `PublishHandler`, `RPCHandler`, `SubRefreshHandler`, `MapPublishHandler`, `MapRemoveHandler`, `SharedPollRefreshHandler`, and Centrifugo PRO's `NotifyCacheEmptyHandler` and `NotifyChannelStateHandler` - with the request and result types generated from `proxy.proto`, `ConnectResult#profile` included. A block returns a result, or raises `Cent::Proxy::ProxyError` or `Cent::Proxy::Disconnect`; anything else, or a block returning nil, is a 500 reported to `on_error:`. Each is a Rack app.
81
+
82
+ **Commands for async consumers**
83
+
84
+ * `Cent::Command.publish(...)` and a method like it for every API method, checked as a call is: `method` and `payload` for an outbox table, `to_json` for a Kafka message, `as_json` with the payload as a Hash.
85
+
86
+ **Observability**
87
+
88
+ * `on_request:` is called after every HTTP request with a `Cent::RequestInfo`: method, commands, failed, automatic batch or not, duration, status, error.
89
+
90
+ **Testing**
91
+
92
+ * A fake `http_client:` answers as Centrifugo does, without stubbing HTTP. `Cent::Pending.resolved` and the public constructors of `BroadcastFailedChannelsError`, `BatchFailedCommandsError`, `BroadcastResult` and `ChannelResult` make test doubles of your own.
93
+
1
94
  3.0.0
2
95
  =====
3
96
 
data/MIGRATING.md ADDED
@@ -0,0 +1,305 @@
1
+ # Migrating from cent 4 to cent 5
2
+
3
+ cent 5 is a rewrite, to the
4
+ [Centrifugo server SDK specification](https://github.com/centrifugal/gocent/blob/master/spec.md),
5
+ version 0.4.
6
+ It needs Ruby 3.4 or newer and Centrifugo v6, and depends on nothing but the
7
+ json gem: Faraday and the jwt gem are gone. An application which needs time to
8
+ move can stay on `gem "cent", "~> 4.0"`, which keeps working.
9
+
10
+ What changes:
11
+
12
+ - **Every API method** still takes keyword arguments, now every field of its
13
+ request - generated from Centrifugo's `api.proto`, each documented - and
14
+ returns a typed result: `res.offset` instead of `res["result"]["offset"]`.
15
+ Every method of Centrifugo v6's API is there, the PRO ones included.
16
+ - **Errors** are Centrifugo's codes as classes - `Cent::UnknownChannelError`,
17
+ `Cent::NotFoundError`, ... all `Cent::ApiError` - instead of
18
+ `Cent::ResponseError`. **Broadcasts and batches raise** when parts of them
19
+ failed: see [below](#broadcasts-and-batches-now-raise).
20
+ - **The client** takes `api_endpoint:`, which is required, and
21
+ `request_timeout:`, which bounds every call whole; a call's `timeout:`
22
+ replaces it. The Faraday block is gone: TLS settings go in `http_options:`,
23
+ extra headers in `headers:`, another HTTP library in `http_client:`.
24
+ - **Requests are checked before sending**: a missing or empty required field,
25
+ a publication without data, a payload which is not JSON raise
26
+ `Cent::InvalidRequestError` instead of reaching Centrifugo: see
27
+ [below](#other-changes-you-may-notice).
28
+ - **Tokens** are made with `Cent::Token.connection` and
29
+ `Cent::Token.subscription`, with every claim Centrifugo reads, and HMAC, RSA,
30
+ ECDSA or Ed25519 keys. An expiry is required: `:never` for a token accepted
31
+ forever.
32
+ - **New:** batches with a result per command, automatic batching, retries,
33
+ proxy handlers and the endpoints client SDKs call as Rack apps, JWKS,
34
+ Centrifugo PRO's API tokens, shared poll signatures, commands for async
35
+ consumers.
36
+
37
+ ## Broadcasts and batches now raise
38
+
39
+ This is the change most likely to break code which moved over unchanged.
40
+
41
+ cent 4 returned Centrifugo's response for a broadcast or a batch, and a
42
+ channel or command which failed was only an `"error"` inside it. Code which
43
+ didn't look - most code - went on as if everything had been published.
44
+
45
+ cent 5 raises when any part failed, even when the others succeeded:
46
+ `Cent::BroadcastFailedChannelsError` for a broadcast,
47
+ `Cent::BatchFailedCommandsError` for a batch. Not rescuing it means an error
48
+ where cent 4 was silent - in a web request, an error page, although most
49
+ channels got the publication.
50
+
51
+ Decide, for each broadcast and batch, which you want:
52
+
53
+ - **A failure is an error** - an unknown channel is a bug, a broker failure
54
+ needs attention: let it raise, and your error reporting sees it.
55
+ - **Best effort, as cent 4 behaved** - publish where it can, log the rest:
56
+ rescue the error. It holds every part's outcome, and the parts which
57
+ succeeded were published.
58
+
59
+ ```ruby
60
+ begin
61
+ client.broadcast(channels:, data:)
62
+ rescue Cent::BroadcastFailedChannelsError => e
63
+ e.failed.each { logger.warn("broadcast to #{it.channel} failed: #{it.error.message}") }
64
+ end
65
+ ```
66
+
67
+ ## Other changes you may notice
68
+
69
+ **Timeouts bound the whole call.** cent 4's `timeout:` set Faraday's open and
70
+ read timeouts, each operation's own, so a reply arriving slowly could take
71
+ longer. cent 5's `request_timeout:` - 10 seconds by default - bounds every
72
+ call whole, a reply's headers trickling in included, and raises
73
+ `Cent::TimeoutError` at it. A call's `timeout:` replaces it, longer or shorter;
74
+ zero is refused, so a call cannot be left unbounded:
75
+
76
+ ```ruby
77
+ # cent 4
78
+ client = Cent::Client.new(api_key: "k", endpoint: "http://localhost:8000/api", timeout: 5)
79
+ # cent 5
80
+ client = Cent::Client.new(api_endpoint: "http://localhost:8000/api", api_key: "k", request_timeout: 5)
81
+ client.history(channel: "news", limit: 1000, timeout: 30) # this call may take longer
82
+ ```
83
+
84
+ **Wrong requests fail before sending.** cent 4 sent what it was given, and
85
+ Centrifugo answered with an error - `Cent::ResponseError`, code 107 or 102.
86
+ cent 5 raises `Cent::InvalidRequestError` without sending: a field the protos
87
+ mark required left out or empty (`channel: ""`), a publication without `data`
88
+ or `b64data`, a payload which is not JSON (NaN, a `Time`, a binary string). It
89
+ is not retryable: fix the call.
90
+
91
+ ```ruby
92
+ # cent 4: sent; Centrifugo refused it with Cent::ResponseError
93
+ client.publish(channel: "", data: { text: "hi" })
94
+ # cent 5: not sent
95
+ client.publish(channel: "", data: { text: "hi" }) # raises Cent::InvalidRequestError: channel is required
96
+ ```
97
+
98
+ **nil is no value.** A keyword given nil is not sent, as in cent 4 - so
99
+ `publish(data: nil)` now raises `InvalidRequestError`. To publish JSON null,
100
+ pass `data: JSON::Fragment.new("null")`. A nested message with nothing set is
101
+ left out too: `since: {}` is not sent.
102
+
103
+ **Nested messages take Symbols.** `since: { "offset" => 5 }` is refused,
104
+ naming the fix: write `since: { offset: 5, epoch: "x" }`, or
105
+ `Cent::StreamPosition.new(offset: 5, epoch: "x")`. A position also comes from
106
+ a result: `client.publish(...).position`.
107
+
108
+ **The endpoint has no default.** cent 4 used `http://localhost:8000/api` when
109
+ `endpoint:` was left out; cent 5 requires `api_endpoint:`, and refuses one
110
+ with a query, credentials or a method name at the end (`/api/publish`), with
111
+ `Cent::InvalidConfigError`. `api_key:` is optional: nil when requests are
112
+ authenticated another way.
113
+
114
+ **Headers go in `headers:`.** Without the Faraday block, extra headers - a
115
+ `User-Agent` of your own, which replaces cent's, a header a proxy needs - go in
116
+ `headers: { "User-Agent" => "my-app/1.0" }`. `X-API-Key`, `Authorization` and
117
+ `Content-Type` are refused there: use `api_key:` or `bearer_token_func:`.
118
+
119
+ **HMAC tokens are HS256.** `Cent::Notary.new(secret:, algorithm: "HS512")` has
120
+ no counterpart: `Cent::Token::Key.hmac(secret)` signs HS256, which every
121
+ Centrifugo verifies. RSA, ECDSA and Ed25519 keys are `Key.rsa`, `Key.ecdsa`,
122
+ `Key.ed25519`, or `Key.from_pem`.
123
+
124
+ ## Side by side
125
+
126
+ Each pair prints the same, run against Centrifugo.
127
+
128
+ ### Calls and errors
129
+
130
+ cent 4:
131
+
132
+ ```ruby
133
+ require "cent"
134
+
135
+ client = Cent::Client.new(api_key: "api_key", endpoint: "http://localhost:8000/api", timeout: 5)
136
+
137
+ res = client.publish(channel: "news", data: { text: "hello" })
138
+ puts "published at offset #{res["result"]["offset"]}"
139
+
140
+ res = client.history(channel: "news", limit: 1, reverse: true)
141
+ puts "last: #{res["result"]["publications"][0]["data"]["text"]}"
142
+
143
+ # Centrifugo's errors are raised.
144
+ begin
145
+ client.publish(channel: "unknown:news", data: { text: "hello" })
146
+ rescue Cent::ResponseError => e
147
+ puts "error #{e.code}: #{e.message}"
148
+ end
149
+ ```
150
+
151
+ cent 5:
152
+
153
+ ```ruby
154
+ require "cent"
155
+
156
+ client = Cent::Client.new(api_endpoint: "http://localhost:8000/api", api_key: "api_key", request_timeout: 5)
157
+
158
+ res = client.publish(channel: "news", data: { text: "hello" })
159
+ puts "published at offset #{res.offset}"
160
+
161
+ res = client.history(channel: "news", limit: 1, reverse: true)
162
+ puts "last: #{res.publications[0].data["text"]}"
163
+
164
+ # Centrifugo's errors are raised, a class per code.
165
+ begin
166
+ client.publish(channel: "unknown:news", data: { text: "hello" })
167
+ rescue Cent::ApiError => e
168
+ puts "error #{e.code}: #{e.server_message}"
169
+ end
170
+ ```
171
+
172
+ ### Broadcast
173
+
174
+ A channel which failed is an error to rescue, not an entry to look for.
175
+
176
+ cent 4:
177
+
178
+ ```ruby
179
+ require "cent"
180
+
181
+ client = Cent::Client.new(api_key: "api_key", endpoint: "http://localhost:8000/api")
182
+
183
+ channels = ["news", "unknown:alerts", "sport"]
184
+ res = client.broadcast(channels:, data: { text: "hello" })
185
+ res["result"]["responses"].each_with_index do |r, i|
186
+ if r["error"]
187
+ puts "#{channels[i]}: error #{r["error"]["code"]}"
188
+ else
189
+ puts "#{channels[i]}: offset #{r["result"]["offset"]}"
190
+ end
191
+ end
192
+ ```
193
+
194
+ cent 5:
195
+
196
+ ```ruby
197
+ require "cent"
198
+
199
+ client = Cent::Client.new(api_endpoint: "http://localhost:8000/api", api_key: "api_key")
200
+
201
+ begin
202
+ res = client.broadcast(channels: ["news", "unknown:alerts", "sport"], data: { text: "hello" })
203
+ rescue Cent::BroadcastFailedChannelsError => e
204
+ # Some channels failed; the others got the publication.
205
+ res = e.result
206
+ end
207
+ res.channels.each do |ch|
208
+ if ch.error
209
+ puts "#{ch.channel}: error #{ch.error.code}"
210
+ else
211
+ puts "#{ch.channel}: offset #{ch.result.offset}"
212
+ end
213
+ end
214
+ ```
215
+
216
+ ### Batch
217
+
218
+ A batch has the client's methods, each adding a command with its own result.
219
+
220
+ cent 4:
221
+
222
+ ```ruby
223
+ require "cent"
224
+
225
+ client = Cent::Client.new(api_key: "api_key", endpoint: "http://localhost:8000/api")
226
+
227
+ res = client.batch(commands: [
228
+ { "publish" => { "channel" => "news", "data" => { "text" => "hi" } } },
229
+ { "presence_stats" => { "channel" => "news" } }
230
+ ])
231
+ puts "published at offset #{res["replies"][0]["publish"]["offset"]}"
232
+ puts "clients: #{res["replies"][1]["presence_stats"]["num_clients"] || 0}"
233
+ ```
234
+
235
+ cent 5:
236
+
237
+ ```ruby
238
+ require "cent"
239
+
240
+ client = Cent::Client.new(api_endpoint: "http://localhost:8000/api", api_key: "api_key")
241
+
242
+ batch = client.new_batch
243
+ pub = batch.publish(channel: "news", data: { text: "hi" })
244
+ stats = batch.presence_stats(channel: "news")
245
+ batch.send
246
+ puts "published at offset #{pub.value.offset}"
247
+ puts "clients: #{stats.value.num_clients}"
248
+ ```
249
+
250
+ ### Tokens
251
+
252
+ The claims are named for what they mean; the expiry is a `Time`, or `:never`.
253
+
254
+ cent 4:
255
+
256
+ ```ruby
257
+ require "cent"
258
+ require "json"
259
+
260
+ notary = Cent::Notary.new(secret: "secret")
261
+ token = notary.issue_connection_token(sub: "42", exp: 1_900_000_000, info: { name: "Alice" }, channels: ["news"])
262
+ puts JSON.parse(token.split(".")[1].unpack1("m")).slice("sub", "exp", "info", "channels")
263
+
264
+ token = notary.issue_channel_token(sub: "42", channel: "news", exp: 1_900_000_000)
265
+ puts JSON.parse(token.split(".")[1].unpack1("m")).slice("sub", "exp", "channel")
266
+ ```
267
+
268
+ cent 5:
269
+
270
+ ```ruby
271
+ require "cent"
272
+
273
+ key = Cent::Token::Key.hmac("secret")
274
+ token = Cent::Token.connection(key, user: "42", expires_at: Time.at(1_900_000_000), info: { name: "Alice" },
275
+ channels: ["news"])
276
+ puts JSON.parse(token.split(".")[1].unpack1("m")).slice("sub", "exp", "info", "channels")
277
+
278
+ token = Cent::Token.subscription(key, user: "42", channel: "news", expires_at: Time.at(1_900_000_000))
279
+ puts JSON.parse(token.split(".")[1].unpack1("m")).slice("sub", "exp", "channel")
280
+ ```
281
+
282
+ ## Mapping
283
+
284
+ | cent 4 | cent 5 |
285
+ |---|---|
286
+ | `Cent::Client.new(api_key:, endpoint:, timeout:)` | `Cent::Client.new(api_endpoint:, api_key:, request_timeout:)`; `api_endpoint` is required, without a default; `request_timeout` bounds every call whole |
287
+ | the Faraday block: `conn.options...`, `conn.adapter` | `http_options: { ca_file:, cert:, key:, ... }`, or `http_client:` for another HTTP library; `timeout:` on a call replaces `request_timeout` for it |
288
+ | `conn.headers["User-Agent"] = ...` | `headers: { "User-Agent" => ... }` |
289
+ | `Cent::ResponseError` for a request Centrifugo refused as invalid (code 107) | often `Cent::InvalidRequestError`, raised before sending |
290
+ | `publish(channel:, data:, ...)` and the other methods | the same names and keywords, every field of the request; a result object instead of the response Hash |
291
+ | `history(..., since: { "offset" => 5, "epoch" => "x" })` | `since: { offset: 5, epoch: "x" }`, or `Cent::StreamPosition.new(...)`: field names are Symbols |
292
+ | `disconnect(whitelist:)` | gone from Centrifugo v6 |
293
+ | `broadcast(...)` with errors in `responses` | raises `BroadcastFailedChannelsError` when a channel failed; `broadcast_with_retry` |
294
+ | `batch(commands: [...])` | `batch = client.new_batch; p = batch.publish(...); batch.send; p.value` |
295
+ | `Cent::ResponseError` (`code`, `message`) | `Cent::ApiError` (`code`, `server_message`, `temporary?`), a subclass per known code |
296
+ | `Cent::TransportError` (`status`) | `Cent::HTTPError` (`status_code`, `body`, `headers`) |
297
+ | `Cent::TimeoutError`, `Cent::NetworkError`, `Cent::UnauthorizedError`, `Cent::DecodeError` | the same names |
298
+ | `Cent::Notary.new(secret:, algorithm:)` | `Cent::Token::Key.hmac(secret)`, `.rsa(pkey)`, `.ecdsa(pkey)`, `.ed25519(pkey)`, `.from_pem(pem)`; HMAC tokens are HS256 |
299
+ | `issue_connection_token(sub:, exp:, iat:, jti:, aud:, iss:, ...)` | `Cent::Token.connection(key, user:, expires_at:, issued_at:, id:, audience:, issuer:, ...)` |
300
+ | `issue_channel_token(sub:, channel:, ...)` | `Cent::Token.subscription(key, user:, channel:, expires_at:, ...)` |
301
+ | a token without `exp`, which never expired | `expires_at: :never`, chosen explicitly |
302
+ | `expire_at:` claims as Unix seconds | `connection_expires_at:`, `subscription_expires_at:` as a `Time`, or `:never` |
303
+
304
+ For tests, give the client a fake `http_client:` instead of stubbing HTTP: a
305
+ callable returning `[status, headers, body]`.