relaygrid 0.1.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0d0cc0707a44f80cc25e92aad237601e2fc3ec3f8ea2bd455facb68180843196
4
+ data.tar.gz: 237cd1b6377a7b4a8fc3ba57287da93990620efc3c6971af67277defcd50b886
5
+ SHA512:
6
+ metadata.gz: 289569fcadc3c5d298ea4450a0ee00c33e21db743725ef9116aff25206b5c5fcf10ae3564434fb6a8a08af8b31c138e67a9cf79e64debcbef9d5616ba51e3672
7
+ data.tar.gz: 30de0ca4b93b88a2740787d1ce97eba72bf17807fe9af59eb2df99041909a6d86916a76f64debc45a14af950c38dfb70627b9048898d06c410ec4580d7e4c37a
data/CHANGELOG.md ADDED
@@ -0,0 +1,87 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.1] - 2026-07-30
11
+
12
+ ### Removed
13
+
14
+ - `users.sms_opted_out?`, and `phone` is no longer forwarded from the `user:`
15
+ payload `notify` accepts. Contact management covers email; passing `phone:`
16
+ is now silently ignored rather than sent. Technically breaking for anyone who
17
+ reached for either in the hours 0.1.0 was current.
18
+
19
+ ### Changed
20
+
21
+ - Contact documentation and examples cover email throughout.
22
+
23
+ ## [0.1.0] - 2026-07-30
24
+
25
+ First public release.
26
+
27
+ ### Added
28
+
29
+ - `RelayGrid.client.notify(user:, template:, attributes:)` — the one-call send.
30
+ Creates or updates the recipient server-side, and returns a `SendResult` with
31
+ the message id, delivery ids, and a push token when the template has a live
32
+ push channel.
33
+ - `SendResult` and `Delivery` value objects. Both immutable; `Delivery` carries
34
+ status predicates (`queued?`, `sent?`, `delivered?`, `failed?`, `bounced?`,
35
+ `opened?`, `success?`) and `#refresh`.
36
+ - `deliveries.get(id)` and `deliveries.get_all(ids)` for delivery status, backed
37
+ by the new `GET /api/v1/deliveries` endpoints. `get_all` batches in chunks of
38
+ 100 to match the server's cap.
39
+ - `result.wait_for_deliveries(timeout:, interval:)` — blocking poll until every
40
+ delivery settles. `failed` is not treated as terminal, since delivery jobs
41
+ retry.
42
+ - `messages.new_for(user_id:)`, `mark_as_seen`, `mark_as_delivered` for the
43
+ receive side, addressed by your own user id rather than an internal one.
44
+ - `RelayGrid.websocket(user_id:)` — Action Cable subscriber for `MessagesChannel`
45
+ that mints a fresh channel token on every connect and reconnect.
46
+ - `channel_tokens.create(user_id:)` to refresh a push token after its hour.
47
+ - `users` resource (`list`, `get`, `upsert`, `delete`) for managing recipients
48
+ outside a send, plus contact management — `contacts`, `set_contact` and
49
+ `delete_contact`.
50
+ - `notify` forwards `user[:email]`, which the server upserts as the contact
51
+ email deliveries resolve their recipient through. Omitting the key leaves that
52
+ contact untouched; an empty string removes it.
53
+ - Full error hierarchy under `RelayGrid::Error`, including `LimitExceededError`
54
+ (402) and `TemplateNotFoundError` / `UserNotFoundError` discriminated from a
55
+ 404's message. Faraday exceptions no longer escape the gem.
56
+ - Automatic retries with jittered backoff for idempotent (GET) requests.
57
+ - `ca_file` configuration for trusting a private CA.
58
+ - RuboCop (Omakase, matching the backend) and a GitHub Actions matrix on Ruby
59
+ 3.1/3.2/3.3.
60
+
61
+ ### Changed
62
+
63
+ - **Renamed from `notify_hub` to `relaygrid`.** The module is now `RelayGrid`,
64
+ the require path is `relaygrid`, and environment variables are prefixed
65
+ `RELAYGRID_`. This settles the naming question ahead of the first release.
66
+ - `Client` instances are frozen after construction and safe to share across
67
+ threads.
68
+ - `Client.new` accepts per-client overrides (`Client.new(api_key: "...")`) for
69
+ apps serving multiple accounts.
70
+ - Configuration validates eagerly: a blank `api_key` or malformed `base_url`
71
+ raises `ConfigurationError` when the client is built.
72
+ - Minimum Ruby is now 3.1.
73
+
74
+ ### Removed
75
+
76
+ - `config.account_id`. The account is resolved from the API key server-side, so
77
+ it was never needed on the wire.
78
+ - `config.verify_ssl`. TLS certificates are always verified; use `ca_file` to
79
+ trust a private CA. A switch to disable verification is not something this
80
+ client should offer, given it carries an API key and relays channel tokens.
81
+ - `actor_email` on sends. The API does not accept the parameter, so passing it
82
+ was silently a no-op.
83
+
84
+ ### Dependencies
85
+
86
+ - Added `faraday-retry`.
87
+ - Dropped `faraday-multipart` and `faraday-follow_redirects` (unused).
data/QUICKSTART.md ADDED
@@ -0,0 +1,112 @@
1
+ # RelayGrid Gem — Quick Start
2
+
3
+ From zero to a delivered notification. The [README](README.md) has the full
4
+ reference.
5
+
6
+ ## 1. Install
7
+
8
+ ```ruby
9
+ # Gemfile
10
+ gem "relaygrid"
11
+ ```
12
+
13
+ ```bash
14
+ bundle install
15
+ ```
16
+
17
+ ## 2. Get an API key
18
+
19
+ Dashboard → **API Keys** → *New API Key*. Copy it — it is shown once. The key
20
+ identifies your account, so there is nothing else to configure.
21
+
22
+ ```bash
23
+ # .env
24
+ RELAYGRID_API_KEY=sk_your_key_here
25
+ ```
26
+
27
+ ## 3. Configure
28
+
29
+ ```ruby
30
+ # config/initializers/relaygrid.rb
31
+ RelayGrid.configure do |config|
32
+ config.api_key = ENV["RELAYGRID_API_KEY"]
33
+ config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
34
+ end
35
+ ```
36
+
37
+ ## 4. Create a template
38
+
39
+ Dashboard → **Templates** → *New Template*. Give it a name you'll pass to
40
+ `notify` (say `order_shipped`), write the subject and body with `{{placeholders}}`,
41
+ attach at least one channel, and set it **active**.
42
+
43
+ ```
44
+ Name: order_shipped
45
+ Subject: Hi {{first_name}}, order {{order_number}} shipped
46
+ Body: It should arrive {{eta}}.
47
+ ```
48
+
49
+ ## 5. Send
50
+
51
+ ```ruby
52
+ result = RelayGrid.client.notify(
53
+ user: { id: current_user.id, first_name: current_user.first_name, last_name: current_user.last_name },
54
+ template: "order_shipped",
55
+ attributes: { first_name: current_user.first_name, order_number: order.number, eta: "tomorrow" }
56
+ )
57
+ ```
58
+
59
+ `user[:id]` is **your** identifier. The recipient is created on first send, so
60
+ there's no registration step.
61
+
62
+ ## 6. Check what happened
63
+
64
+ ```ruby
65
+ result.delivery_ids # => [201, 202] — keep these
66
+
67
+ delivery = RelayGrid.client.deliveries.get(201)
68
+ delivery.status # => "delivered"
69
+ delivery.delivered? # => true
70
+ ```
71
+
72
+ Store `delivery_ids` alongside whatever triggered the notification, and poll
73
+ them when you want status. Don't block a web request waiting — see
74
+ `wait_for_deliveries` in the README for when that helper is and isn't
75
+ appropriate.
76
+
77
+ ## 7. Receive (optional)
78
+
79
+ If your template has a push channel, the send hands you a token for the
80
+ recipient's device:
81
+
82
+ ```ruby
83
+ result.push_token # => "eyJf..." — pass to your frontend; expires in 1 hour
84
+ ```
85
+
86
+ Server-side, poll or subscribe:
87
+
88
+ ```ruby
89
+ RelayGrid.client.messages.new_for(user_id: current_user.id)
90
+
91
+ ws = RelayGrid.websocket(user_id: current_user.id)
92
+ ws.on_message { |message| puts message["rendered_subject"] }
93
+ ws.connect
94
+ ```
95
+
96
+ ## Troubleshooting
97
+
98
+ | Problem | Cause |
99
+ |---|---|
100
+ | `ConfigurationError: api_key is required` | `RELAYGRID_API_KEY` isn't set in this environment. |
101
+ | `AuthenticationError` | Key is wrong, deactivated, expired, or the account is suspended. |
102
+ | `TemplateNotFoundError` | Template name doesn't match, or the template is soft-deleted. |
103
+ | `LimitExceededError` | Monthly notification limit spent — check Usage in the dashboard. |
104
+ | `result.push_token` is `nil` | The template has no live push channel attached. |
105
+ | Delivery stuck at `queued` | The delivery job hasn't run yet; poll again shortly. |
106
+ | Delivery `failed` | Read `delivery.friendly_error_message` — usually channel credentials. |
107
+
108
+ ## Next steps
109
+
110
+ - [README](README.md) — full configuration, error hierarchy, retry semantics
111
+ - [examples/usage_examples.rb](examples/usage_examples.rb) — runnable end-to-end script
112
+ - [examples/rails_initializer.rb](examples/rails_initializer.rb) — annotated initializer
data/README.md ADDED
@@ -0,0 +1,249 @@
1
+ # RelayGrid Ruby Gem
2
+
3
+ Send and receive notifications through [RelayGrid](https://relaygrid.com) in one call.
4
+
5
+ Give it a user, a template name, and the attributes the template needs. Get back
6
+ a push token for the recipient's device and the delivery ids you can check later.
7
+
8
+ ```ruby
9
+ result = RelayGrid.client.notify(
10
+ user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
11
+ template: "order_shipped",
12
+ attributes: { order_number: "1042", eta: "tomorrow" }
13
+ )
14
+
15
+ result.message_id # => 87
16
+ result.delivery_ids # => [201, 202]
17
+ result.push_token # => "eyJf..." (nil when the template has no push channel)
18
+ ```
19
+
20
+ ## Installation
21
+
22
+ ```ruby
23
+ gem "relaygrid"
24
+ ```
25
+
26
+ Then `bundle install`. Requires Ruby >= 3.1.
27
+
28
+ ## Configuration
29
+
30
+ ```ruby
31
+ # config/initializers/relaygrid.rb
32
+ RelayGrid.configure do |config|
33
+ config.api_key = ENV["RELAYGRID_API_KEY"]
34
+ config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
35
+ end
36
+ ```
37
+
38
+ | Option | Default | Purpose |
39
+ |---|---|---|
40
+ | `api_key` | — | Account API key (`sk_...`). Required. |
41
+ | `base_url` | `https://relaygrid.dev` | API origin. |
42
+ | `timeout` | `30` | Seconds to wait for a response. |
43
+ | `open_timeout` | `10` | Seconds to wait for the connection to open. |
44
+ | `max_retries` | `3` | Retry attempts for idempotent (GET) requests. |
45
+ | `logger` | `nil` | Logs requests when set. Headers and bodies are not logged. |
46
+ | `ca_file` | `nil` | PEM bundle for a private CA. |
47
+ | `ws_url` | derived from `base_url` | Overrides the Action Cable URL. |
48
+
49
+ Bad configuration raises `RelayGrid::ConfigurationError` when the client is
50
+ built, not on the first request.
51
+
52
+ **TLS.** Certificates are always verified. There is no option to turn
53
+ verification off: the client carries your API key and relays channel tokens, so
54
+ an unverified connection is never the right default. To trust a self-signed or
55
+ internal CA, point `ca_file` at its PEM bundle.
56
+
57
+ ### Multiple accounts
58
+
59
+ `RelayGrid.client` is the shared default. For a host app talking to several
60
+ accounts, build clients with per-client overrides:
61
+
62
+ ```ruby
63
+ client = RelayGrid::Client.new(api_key: account.relaygrid_key)
64
+ ```
65
+
66
+ Clients are frozen once built and safe to share across threads.
67
+
68
+ ## Sending
69
+
70
+ `user[:id]` is **your** identifier for the recipient. RelayGrid stores it as
71
+ `external_user_id` and never exposes its own internal ids to you.
72
+
73
+ ```ruby
74
+ result = RelayGrid.client.notify(
75
+ user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
76
+ template: "order_shipped",
77
+ attributes: { order_number: "1042" }
78
+ )
79
+ ```
80
+
81
+ The recipient is created on first send and their names refreshed when they
82
+ change, so there is no separate registration step. If the recipient already
83
+ exists and you don't want to send names, pass the id on its own:
84
+
85
+ ```ruby
86
+ RelayGrid.client.notify(user: "user-123", template: "order_shipped")
87
+ ```
88
+
89
+ ### Email recipients
90
+
91
+ Push needs nothing but the id. Email deliveries resolve **who** to send to from
92
+ the address stored for the recipient, so include it in the send:
93
+
94
+ ```ruby
95
+ RelayGrid.client.notify(
96
+ user: { id: "user-123", first_name: "Ada", last_name: "Lovelace",
97
+ email: "ada@example.com" },
98
+ template: "order_shipped"
99
+ )
100
+ ```
101
+
102
+ Without it, a send over an email channel produces a delivery that fails with
103
+ "no contact on file". A value RelayGrid can't use raises `ValidationError`
104
+ rather than failing quietly at delivery time.
105
+
106
+ Omitting the key leaves the existing address alone; passing an empty string
107
+ removes it. Contacts can also be managed outside a send:
108
+
109
+ ```ruby
110
+ RelayGrid.client.users.contacts("user-123")
111
+ RelayGrid.client.users.set_contact("user-123", channel_type: "email", value: "ada@example.com")
112
+ RelayGrid.client.users.delete_contact("user-123", channel_type: "email")
113
+ ```
114
+
115
+ ### The result
116
+
117
+ ```ruby
118
+ result.message_id # => 87
119
+ result.delivery_ids # => [201, 202]
120
+ result.deliveries # => [#<RelayGrid::Delivery id: 201, channel_type: "push", status: "queued">, ...]
121
+ result.delivery_for("email") # => #<RelayGrid::Delivery ...>
122
+ result.rendered_subject # => "Hi Ada"
123
+ result.push_token # => "eyJf..." or nil
124
+ result.push_token_expires_at # => 2026-07-28 11:00:00 +0000
125
+ result.push? # => true when a push channel was dispatched
126
+ ```
127
+
128
+ ### The push token
129
+
130
+ When the template has a live push channel, the send response carries a token
131
+ that authenticates **the recipient's device** to the realtime channel. Your
132
+ server receives it and hands it down to your client. It expires after an hour;
133
+ mint a fresh one with:
134
+
135
+ ```ruby
136
+ RelayGrid.client.channel_tokens.create(user_id: "user-123")
137
+ # => { token: "eyJf...", expires_in: 3600, expires_at: 2026-07-28 11:00:00 +0000 }
138
+ ```
139
+
140
+ ## Checking delivery status
141
+
142
+ ```ruby
143
+ delivery = RelayGrid.client.deliveries.get(201)
144
+ delivery.status # => "delivered"
145
+ delivery.delivered? # => true
146
+ delivery.channel_type # => "push"
147
+ delivery.friendly_error_message # => nil, or human-readable guidance when failed
148
+
149
+ RelayGrid.client.deliveries.get_all([201, 202]) # one batched request
150
+ ```
151
+
152
+ Predicates: `queued?`, `sent?`, `delivered?`, `failed?`, `bounced?`, `opened?`,
153
+ plus `success?` (delivered or opened). `#refresh` re-fetches and returns a new
154
+ `Delivery` — the objects are immutable.
155
+
156
+ ### Waiting for deliveries to settle
157
+
158
+ ```ruby
159
+ settled = result.wait_for_deliveries(timeout: 30, interval: 2)
160
+ settled.all?(&:success?) # => true
161
+ ```
162
+
163
+ > **This blocks the calling thread.** Use it in a background job, a rake task, or
164
+ > a script — never inside a web request, where it would pin a request thread for
165
+ > up to `timeout` seconds. In a request, return `delivery_ids` and let the
166
+ > browser poll, or subscribe over the websocket.
167
+
168
+ `failed` is deliberately **not** treated as final inside the window: delivery
169
+ jobs retry, so a failed delivery can still flip to `sent` and then `delivered`.
170
+ The helper waits rather than reporting a transient failure as permanent.
171
+
172
+ ## Receiving
173
+
174
+ ### Polling
175
+
176
+ ```ruby
177
+ RelayGrid.client.messages.new_for(user_id: "user-123") # unseen messages
178
+ RelayGrid.client.messages.mark_as_seen(message_id)
179
+ RelayGrid.client.messages.mark_as_delivered(message_id)
180
+ ```
181
+
182
+ ### Realtime
183
+
184
+ ```ruby
185
+ ws = RelayGrid.websocket(user_id: "user-123")
186
+ ws.on_message { |message| puts message["rendered_subject"] }
187
+ ws.on_error { |error| Rails.logger.error(error.message) }
188
+ ws.connect
189
+ ```
190
+
191
+ A fresh channel token is minted on every connect and reconnect, so the hourly
192
+ expiry is handled for you. `connect(blocking: true)` blocks the thread — for a
193
+ standalone consumer process, not a web request.
194
+
195
+ ## Errors
196
+
197
+ Everything descends from `RelayGrid::Error`, so `rescue RelayGrid::Error` always
198
+ suffices. Faraday exceptions never escape the gem.
199
+
200
+ | Class | Raised when |
201
+ |---|---|
202
+ | `ConfigurationError` | Bad configuration, at client construction |
203
+ | `ConnectionError` | The request never produced a response |
204
+ | `TimeoutError` | A timeout (subclass of `ConnectionError`) |
205
+ | `AuthenticationError` | 401/403 — key invalid, inactive, expired, or account suspended |
206
+ | `LimitExceededError` | 402 — monthly notification limit spent |
207
+ | `NotFoundError` | 404 |
208
+ | `TemplateNotFoundError` | 404 naming a template (subclass of `NotFoundError`) |
209
+ | `UserNotFoundError` | 404 naming a recipient (subclass of `NotFoundError`) |
210
+ | `ValidationError` | 422 |
211
+ | `ServerError` | 5xx |
212
+ | `TimeoutWaitingForDeliveries` | `wait_for_deliveries` gave up (only with `raise_on_timeout`) |
213
+
214
+ `APIError` subclasses carry `#status`, `#body`, and `#error_message` (the
215
+ server's own message).
216
+
217
+ ```ruby
218
+ begin
219
+ RelayGrid.client.notify(user: user, template: "order_shipped")
220
+ rescue RelayGrid::LimitExceededError
221
+ # out of notifications this month
222
+ rescue RelayGrid::TemplateNotFoundError => e
223
+ Rails.logger.error(e.error_message)
224
+ end
225
+ ```
226
+
227
+ ## Retries and idempotency
228
+
229
+ GETs retry automatically (3 attempts by default, jittered exponential backoff)
230
+ on 429, 5xx, and transport failures.
231
+
232
+ **`notify` is never retried.** A blind retry would send the recipient a second
233
+ real notification. If a send fails, deciding whether to re-send is yours to
234
+ make. Server-side `Idempotency-Key` support is planned so retries can be made
235
+ safe.
236
+
237
+ ## Development
238
+
239
+ ```bash
240
+ bin/setup
241
+ bundle exec rspec
242
+ bundle exec rubocop
243
+ ```
244
+
245
+ Specs use WebMock; no example makes a real HTTP call or sleeps for real.
246
+
247
+ ## License
248
+
249
+ MIT.
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ # config/initializers/relaygrid.rb
4
+ #
5
+ # Example Rails initializer for the RelayGrid gem. Copy it into your app at
6
+ # config/initializers/relaygrid.rb.
7
+
8
+ RelayGrid.configure do |config|
9
+ # Account API key ("sk_..."), from the API Keys page of your dashboard.
10
+ # Required -- the account is resolved from it server-side.
11
+ config.api_key = ENV.fetch("RELAYGRID_API_KEY", nil)
12
+
13
+ # API origin. Point this at your local stack in development.
14
+ config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
15
+
16
+ # Seconds to wait for a response, and for the connection to open.
17
+ config.timeout = ENV.fetch("RELAYGRID_TIMEOUT", 30).to_i
18
+ config.open_timeout = ENV.fetch("RELAYGRID_OPEN_TIMEOUT", 10).to_i
19
+
20
+ # Optional: trust a private CA (self-signed local dev, internal deployments).
21
+ # TLS certificates are always verified -- there is no way to turn that off.
22
+ config.ca_file = ENV.fetch("RELAYGRID_CA_FILE", nil)
23
+
24
+ # Optional: log requests (method, URL, status). Headers and bodies are not
25
+ # logged, so your API key never reaches the log.
26
+ config.logger = Rails.logger if Rails.env.development?
27
+ end
28
+
29
+ # Fail fast on a misconfigured deploy rather than on the first send: building
30
+ # the client validates the configuration.
31
+ begin
32
+ RelayGrid.client
33
+ Rails.logger.info("RelayGrid configured: #{RelayGrid.configuration.base_url}")
34
+ rescue RelayGrid::ConfigurationError => e
35
+ Rails.logger.warn("RelayGrid is not configured: #{e.message}")
36
+ Rails.logger.warn("Notifications will fail until this is corrected.")
37
+ end
@@ -0,0 +1,115 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Example usage of the RelayGrid gem.
4
+ #
5
+ # Run with:
6
+ # RELAYGRID_API_KEY=sk_... ruby examples/usage_examples.rb
7
+
8
+ require "relaygrid"
9
+
10
+ RelayGrid.configure do |config|
11
+ config.api_key = ENV.fetch("RELAYGRID_API_KEY")
12
+ config.base_url = ENV.fetch("RELAYGRID_BASE_URL", "https://relaygrid.dev")
13
+ end
14
+
15
+ # ---------------------------------------------------------------------------
16
+ # 1. Send a notification
17
+ # ---------------------------------------------------------------------------
18
+ # `user[:id]` is your own identifier for the recipient. They are created on the
19
+ # first send and their names refreshed when they change, so there is no separate
20
+ # registration step.
21
+
22
+ result = RelayGrid.client.notify(
23
+ user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
24
+ template: "order_shipped",
25
+ attributes: { order_number: "1042", eta: "tomorrow" }
26
+ )
27
+
28
+ puts "message: #{result.message_id}"
29
+ puts "deliveries: #{result.delivery_ids.inspect}"
30
+ puts "subject: #{result.rendered_subject}"
31
+
32
+ # ---------------------------------------------------------------------------
33
+ # 2. Hand the push token to the recipient's device
34
+ # ---------------------------------------------------------------------------
35
+ # Present only when the template has a live push channel. It authenticates the
36
+ # *device*, not your server, and expires after an hour.
37
+
38
+ if result.push?
39
+ puts "push token: #{result.push_token[0, 12]}... (expires #{result.push_token_expires_at})"
40
+ else
41
+ puts "push token: none (template has no push channel)"
42
+ end
43
+
44
+ # ---------------------------------------------------------------------------
45
+ # 3. Check delivery status later
46
+ # ---------------------------------------------------------------------------
47
+
48
+ result.delivery_ids.each do |id|
49
+ delivery = RelayGrid.client.deliveries.get(id)
50
+ puts "delivery #{delivery.id} (#{delivery.channel_type}): #{delivery.status}"
51
+ puts " #{delivery.friendly_error_message}" if delivery.failed?
52
+ end
53
+
54
+ # One request instead of one per id:
55
+ RelayGrid.client.deliveries.get_all(result.delivery_ids).each do |delivery|
56
+ puts "#{delivery.id} => #{delivery.status}"
57
+ end
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # 4. Wait for the deliveries to settle
61
+ # ---------------------------------------------------------------------------
62
+ # BLOCKING -- fine in a script or background job, never in a web request.
63
+
64
+ settled = result.wait_for_deliveries(timeout: 30, interval: 2)
65
+ puts "all delivered: #{settled.all?(&:success?)}"
66
+
67
+ # ---------------------------------------------------------------------------
68
+ # 5. Read a recipient's inbox
69
+ # ---------------------------------------------------------------------------
70
+
71
+ unseen = RelayGrid.client.messages.new_for(user_id: "user-123")
72
+ puts "unseen: #{unseen.size}"
73
+
74
+ unseen.each do |message|
75
+ puts " #{message['rendered_subject']}"
76
+ RelayGrid.client.messages.mark_as_seen(message["id"])
77
+ end
78
+
79
+ # ---------------------------------------------------------------------------
80
+ # 6. Handle errors
81
+ # ---------------------------------------------------------------------------
82
+ # Everything descends from RelayGrid::Error; no Faraday exception escapes.
83
+
84
+ begin
85
+ RelayGrid.client.notify(user: { id: "user-123", first_name: "Ada", last_name: "Lovelace" },
86
+ template: "no_such_template")
87
+ rescue RelayGrid::TemplateNotFoundError => e
88
+ puts "no such template: #{e.error_message}"
89
+ rescue RelayGrid::LimitExceededError
90
+ puts "monthly notification limit reached"
91
+ rescue RelayGrid::AuthenticationError
92
+ puts "API key rejected"
93
+ rescue RelayGrid::ConnectionError => e
94
+ puts "could not reach RelayGrid: #{e.message}"
95
+ end
96
+
97
+ # ---------------------------------------------------------------------------
98
+ # 7. Receive in realtime
99
+ # ---------------------------------------------------------------------------
100
+ # A fresh channel token is minted on every connect and reconnect, so the hourly
101
+ # expiry is handled for you.
102
+
103
+ ws = RelayGrid.websocket(user_id: "user-123")
104
+ ws.on_message { |message| puts "realtime: #{message['rendered_subject']}" }
105
+ ws.on_error { |error| warn "websocket error: #{error.message}" }
106
+
107
+ # ws.connect(blocking: true) # uncomment to run as a standalone consumer
108
+
109
+ # ---------------------------------------------------------------------------
110
+ # 8. Serve several accounts from one process
111
+ # ---------------------------------------------------------------------------
112
+ # Clients are frozen once built and safe to share across threads.
113
+
114
+ other_account = RelayGrid::Client.new(api_key: ENV.fetch("OTHER_RELAYGRID_API_KEY", "sk_other"))
115
+ puts "second client ready: #{other_account.configuration.base_url}"