letterapp 0.2.0 → 0.2.2

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: 0f2fde121f30391e8ef7d0272b8b8ca855d13f8597f90618ca493969eef045c2
4
- data.tar.gz: 2647178c2d51cb3edbf7aace4aa8c71343321434107d5d55c06e8d75407a05f0
3
+ metadata.gz: 6a28e33d3a0a3f740b0d45a1a53c22983dbd781fba93c87dc8a167ae51075bc6
4
+ data.tar.gz: 643ca3a372bf627e1cea174989880196ce66c722da3ebfdd605e35d4bd8373b8
5
5
  SHA512:
6
- metadata.gz: d5b5f7fa5a456b68b32659ffa690f3550e6f6ae618be9f54cab3462d7e2676d31eaf2ebe75a1ab09c04fc0525ae813fc2c7cece6a25f3ea472efbb317df0dafe
7
- data.tar.gz: cfc610d764cf25e18d0894f14bc440613a72945ebc5441d3bb1d86d706832f4f6b28401581fdc6b3fddcbaa381514e8ad9d599c594ef3bb14877c33fc213d691
6
+ metadata.gz: 334a7ce7651803672d8d46bc37c97924bf18a556513a66d879ab0dfb6cf8f5559db138f3501bccc1e23ca4836071c4512a8c3886ff69eb8759b9273a13865080
7
+ data.tar.gz: 25819b5ad570ceec7b853f19bc2629a6b5a2c5b27e10849a5994e0d6e616c4335b616fd5c0be7e88f33419d60a2f7b73ee61e0d809db52aeb350ab0c32edce82
data/CHANGELOG.md ADDED
@@ -0,0 +1,43 @@
1
+ # Changelog
2
+
3
+ ## 0.2.2 - 2026-10-25
4
+
5
+ ### Fixed
6
+
7
+ - Sending mail through the `:letter` ActionMailer delivery method no longer
8
+ leaks a thread per message. `Client.new` started the batching thread, and
9
+ registered an `at_exit` hook holding the client, even for a client that
10
+ only calls `send_email`; Mail builds a new delivery method, and so a new
11
+ client, for every message. A long-running process kept one idle thread and
12
+ one client per email sent. The thread now starts with the first `identify`,
13
+ `group` or `track` call.
14
+ - A client created before a fork (a Rails initializer under Puma cluster mode
15
+ or Solid Queue) now sends from the forked process as usual. The child
16
+ inherited a dead copy of the parent's thread, so its `identify`, `group` and
17
+ `track` calls waited in the queue until the process exited. The client now
18
+ starts a new thread when the old one is dead.
19
+
20
+ ## 0.2.1 - 2026-10-25
21
+
22
+ ### Fixed
23
+
24
+ - `Letterapp::Client` no longer breaks Active Support's `present?`, `blank?`
25
+ and `presence`. 0.2.0 had a private `blank?(value)` helper that shadowed
26
+ `Object#blank?`, so in a Rails app `client.present?` and `client.presence`
27
+ raised `ArgumentError: wrong number of arguments (given 0, expected 1)`, and
28
+ `client.blank?` raised `NoMethodError`. The helper is now named `missing?`.
29
+
30
+ ## 0.2.0 - 2026-08-20
31
+
32
+ ### Added
33
+
34
+ - `send_email` for transactional email (`POST /v1/send`), with
35
+ `idempotency_key:` for replay-safe retries.
36
+ - `Letterapp::DeliveryMethod`, registered as `:letter` with ActionMailer in a
37
+ Rails app.
38
+ - `Letterapp::Error#code` and `#reason`, from the API's error body.
39
+
40
+ ## 0.1.0 - 2026-05-29
41
+
42
+ - First release: `identify`, `group` and `track`, auto-batched in the
43
+ background, with retries and idempotent message IDs.
@@ -34,8 +34,9 @@ module Letterapp
34
34
  # A Letter client.
35
35
  #
36
36
  # Long-running server (default): identify / group / track enqueue calls that a
37
- # background thread auto-batches and flushes every 100ms or 50 events. Call
38
- # +close+ before the process exits.
37
+ # background thread auto-batches and flushes every 100ms or 50 events. The
38
+ # thread starts with the first queued call. Call +close+ before the process
39
+ # exits.
39
40
  #
40
41
  # Serverless: pass flush_at: 1 and call +flush+ at the end of each invocation.
41
42
  #
@@ -69,9 +70,7 @@ module Letterapp
69
70
  @cond = ConditionVariable.new
70
71
  @send_mutex = Mutex.new
71
72
  @closed = false
72
-
73
- @thread = Thread.new { loop_run }
74
- at_exit { close }
73
+ @thread = nil # see start_worker
75
74
  end
76
75
 
77
76
  # Queue an identify call (creates or updates a contact).
@@ -109,9 +108,9 @@ module Letterapp
109
108
  def send_email(to:, subject:, html: nil, text: nil, from: nil, from_name: nil,
110
109
  reply_to: nil, headers: nil, tag: nil, metadata: nil,
111
110
  idempotency_key: nil)
112
- raise Error, "send_email: to is required" if blank?(to)
113
- raise Error, "send_email: subject is required" if blank?(subject)
114
- raise Error, "send_email: provide html, text, or both" if blank?(html) && blank?(text)
111
+ raise Error, "send_email: to is required" if missing?(to)
112
+ raise Error, "send_email: subject is required" if missing?(subject)
113
+ raise Error, "send_email: provide html, text, or both" if missing?(html) && missing?(text)
115
114
 
116
115
  body = { "to" => to, "subject" => subject }
117
116
  {
@@ -154,10 +153,23 @@ module Letterapp
154
153
  return
155
154
  end
156
155
  @queue << item
156
+ start_worker
157
157
  @cond.signal
158
158
  end
159
159
  end
160
160
 
161
+ # Starts the batching thread (and the at_exit hook that drains it) on the
162
+ # first queued call rather than in +initialize+. Mail builds a new delivery
163
+ # method, and so a new client, for every message; each one used to leave
164
+ # an idle thread behind. Also restarts a dead thread, which is what a
165
+ # forked child inherits from its parent. Called with @mutex held.
166
+ def start_worker
167
+ return if @thread&.alive?
168
+
169
+ at_exit { close } if @thread.nil?
170
+ @thread = Thread.new { loop_run }
171
+ end
172
+
161
173
  def take_batch
162
174
  @mutex.synchronize do
163
175
  return nil if @queue.empty?
@@ -248,7 +260,7 @@ module Letterapp
248
260
  end
249
261
 
250
262
  def serialize_identify(user_id, email, traits, timezone, timestamp, message_id)
251
- raise Error, "identify: user_id is required" if blank?(user_id)
263
+ raise Error, "identify: user_id is required" if missing?(user_id)
252
264
 
253
265
  item = {
254
266
  "type" => "identify",
@@ -264,8 +276,8 @@ module Letterapp
264
276
  end
265
277
 
266
278
  def serialize_group(user_id, account_id, name, traits, timestamp, message_id)
267
- raise Error, "group: user_id is required" if blank?(user_id)
268
- raise Error, "group: account_id is required" if blank?(account_id)
279
+ raise Error, "group: user_id is required" if missing?(user_id)
280
+ raise Error, "group: account_id is required" if missing?(account_id)
269
281
 
270
282
  item = {
271
283
  "type" => "group",
@@ -281,8 +293,8 @@ module Letterapp
281
293
  end
282
294
 
283
295
  def serialize_track(user_id, event, properties, timestamp, message_id)
284
- raise Error, "track: user_id is required" if blank?(user_id)
285
- raise Error, "track: event is required" if blank?(event)
296
+ raise Error, "track: user_id is required" if missing?(user_id)
297
+ raise Error, "track: event is required" if missing?(event)
286
298
 
287
299
  item = {
288
300
  "type" => "track",
@@ -303,7 +315,10 @@ module Letterapp
303
315
  value.to_s
304
316
  end
305
317
 
306
- def blank?(value)
318
+ # Not named +blank?+: Active Support defines Object#blank?, and its
319
+ # +present?+ and +presence+ call it, so a private blank?(value) here would
320
+ # shadow it and make +client.present?+ raise ArgumentError in a Rails app.
321
+ def missing?(value)
307
322
  value.nil? || value.to_s.empty?
308
323
  end
309
324
 
@@ -82,14 +82,13 @@ module Letterapp
82
82
 
83
83
  private
84
84
 
85
+ # Mail builds a new delivery method for every message, so this client
86
+ # lives for one message. That's cheap: it only calls send_email, which
87
+ # bypasses the queue, so the client never starts its batching thread.
85
88
  def client
86
89
  @client ||= Client.new(
87
90
  api_key: settings.fetch(:api_key) { ENV.fetch("LETTER_API_KEY") },
88
- base_url: settings[:base_url] || ENV["LETTER_BASE_URL"] || DEFAULT_BASE_URL,
89
- # This client only ever calls send_email, which bypasses the queue, so
90
- # the batching thread would sit idle forever. Setting flush_at to 1
91
- # keeps it from being started for nothing.
92
- flush_at: 1
91
+ base_url: settings[:base_url] || ENV["LETTER_BASE_URL"] || DEFAULT_BASE_URL
93
92
  )
94
93
  end
95
94
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Letterapp
4
- VERSION = "0.2.0"
4
+ VERSION = "0.2.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: letterapp
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - letter.app
@@ -16,6 +16,7 @@ executables: []
16
16
  extensions: []
17
17
  extra_rdoc_files: []
18
18
  files:
19
+ - CHANGELOG.md
19
20
  - LICENSE
20
21
  - README.md
21
22
  - lib/letterapp.rb
@@ -29,6 +30,7 @@ licenses:
29
30
  metadata:
30
31
  homepage_uri: https://letter.app/docs/ruby-sdk
31
32
  source_code_uri: https://github.com/vincenzor/letter-ruby
33
+ changelog_uri: https://github.com/vincenzor/letter-ruby/blob/main/CHANGELOG.md
32
34
  bug_tracker_uri: https://github.com/vincenzor/letter-ruby/issues
33
35
  rubygems_mfa_required: 'true'
34
36
  rdoc_options: []