monkrb 0.17.0 → 0.19.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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +133 -0
  3. data/README.md +6 -5
  4. data/exe/monk +45 -7
  5. data/lib/monk/jobs/adapters/pg.rb +272 -0
  6. data/lib/monk/jobs/args.rb +43 -0
  7. data/lib/monk/jobs/claim.rb +7 -0
  8. data/lib/monk/jobs/errors.rb +29 -0
  9. data/lib/monk/jobs/job.rb +117 -0
  10. data/lib/monk/jobs/runtime.rb +239 -0
  11. data/lib/monk/jobs/worker.rb +94 -0
  12. data/lib/monk/jobs.rb +229 -0
  13. data/lib/monk/live/client/monk_live.js +2 -2
  14. data/lib/monk/live/client/protocol.js +5 -0
  15. data/lib/monk/live.rb +10 -0
  16. data/lib/monk/mail/errors.rb +7 -0
  17. data/lib/monk/mail/later.rb +53 -0
  18. data/lib/monk/persistence/pg.rb +21 -1
  19. data/lib/monk/scaffold.rb +369 -70
  20. data/lib/monk/templates/auth/app/mailers/app_mailer.rb +22 -0
  21. data/lib/monk/templates/auth/config/auth.rb +7 -20
  22. data/lib/monk/templates/base/app/app.rb +9 -0
  23. data/lib/monk/templates/base/app/broadcasts/.keep +0 -0
  24. data/lib/monk/templates/base/app/helpers/.keep +0 -0
  25. data/lib/monk/templates/base/app/jobs/.keep +0 -0
  26. data/lib/monk/templates/base/app/mailers/.keep +0 -0
  27. data/lib/monk/templates/base/app/models/.keep +0 -0
  28. data/lib/monk/templates/base/app/presenters/.keep +0 -0
  29. data/lib/monk/templates/base/bin/websocket_server +6 -0
  30. data/lib/monk/templates/base/config/load.rb +18 -0
  31. data/lib/monk/templates/base/config.ru +3 -12
  32. data/lib/monk/templates/base/public/js/app.js +1 -1
  33. data/lib/monk/templates/jobs/app/jobs/hello_job.rb +9 -0
  34. data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +28 -0
  35. data/lib/monk/templates/jobs/bin/jobs +18 -0
  36. data/lib/monk/templates/jobs/config/jobs.rb +15 -0
  37. data/lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.down.sql +3 -0
  38. data/lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.up.sql +48 -0
  39. data/lib/monk/templates/live/{config.ru → app/app.rb} +1 -6
  40. data/lib/monk/templates/live/{views → app/views}/index.erb +2 -2
  41. data/lib/monk/templates/live/bin/websocket_server +14 -2
  42. data/lib/monk/templates/live/config/live.rb +1 -1
  43. data/lib/monk/templates/live/config/live_pg.rb +1 -1
  44. data/lib/monk/templates/mail/config/mail.rb +2 -2
  45. data/lib/monk/templates/postgres/Dockerfile +3 -1
  46. data/lib/monk/templates/postgres/bin/console +4 -2
  47. data/lib/monk/version.rb +1 -1
  48. data/lib/monk/websocket/errors.rb +11 -0
  49. data/lib/monk/websocket/listeners.rb +23 -0
  50. data/lib/monk/websocket/pg_fanout.rb +55 -20
  51. data/lib/monk/websocket/redis_fanout.rb +57 -14
  52. data/lib/monk/websocket/registry.rb +6 -0
  53. data/lib/monk/websocket/server.rb +77 -17
  54. metadata +32 -7
  55. /data/lib/monk/templates/auth/{views → app/views}/mail/magic_link.erb +0 -0
  56. /data/lib/monk/templates/base/{views → app/views}/index.erb +0 -0
  57. /data/lib/monk/templates/base/{views → app/views}/layouts/app.erb +0 -0
  58. /data/lib/monk/templates/live/{views → app/views}/live/_hits.erb +0 -0
@@ -8,6 +8,11 @@ export const MAX_DELAY = 30000;
8
8
  // Reconnect backoff: double, capped.
9
9
  export const nextDelay = (delay) => Math.min(delay * 2, MAX_DELAY);
10
10
 
11
+ // The wait actually used: 50%..150% of the backoff delay. When the WS process
12
+ // restarts, every open page loses its socket at once; without jitter they all
13
+ // reconnect, and all refetch their page (resync), at the same instant.
14
+ export const jittered = (delay, random = Math.random) => Math.floor(delay * (0.5 + random()));
15
+
11
16
  const MODES = ["morph", "replace", "append", "prepend", "remove"];
12
17
 
13
18
  // data-live-topic values ("a:1 b:2") -> a sorted, de-duplicated topic list.
data/lib/monk/live.rb CHANGED
@@ -62,6 +62,16 @@ module Monk
62
62
  @max_topics = max_topics
63
63
  end
64
64
 
65
+ # Boot-time, main Ractor, in the WS process only (bin/websocket_server):
66
+ # starts the configured registry relaying other processes' updates
67
+ # to this process's sockets. A publish-only process (bin/server,
68
+ # bin/jobs) never calls it, so it opens no subscriber connection.
69
+ def listen!
70
+ registry || raise(NotConfiguredError,
71
+ "Monk::Live isn't configured -- call Monk::Live.configure(registry: ...) before listen!",)
72
+ registry.listen!
73
+ end
74
+
65
75
  # Boot-time, main Ractor. See Monk::Live::Policy for the semantics:
66
76
  # deny by default, first matching rule wins, anonymous subjects need
67
77
  # `anonymous: true`. The block must be Ractor-shareable.
@@ -24,5 +24,12 @@ module Monk
24
24
  # refusing it. The transport's own exception is the #cause.
25
25
  class DeliveryError < StandardError
26
26
  end
27
+
28
+ # A DeliveryError the server made final: an SMTP 5xx (no such mailbox,
29
+ # relaying refused) or refused credentials. Raised by
30
+ # Monk::Mail::DeliveryJob (require "monk/mail/later") so the job isn't
31
+ # retried; still a DeliveryError, so a rescue of that catches it too.
32
+ class PermanentDeliveryError < DeliveryError
33
+ end
27
34
  end
28
35
  end
@@ -0,0 +1,53 @@
1
+ require_relative "../mail"
2
+ require_relative "../jobs"
3
+
4
+ module Monk
5
+ module Mail
6
+ # Sends one message from a job process (docs/adr/0014-mail-from-jobs-and-login-links-created-in-the-job.md).
7
+ # Enqueued by Monk::Mail.deliver_later, never directly: its argument
8
+ # is the message's fields, already checked and with from: resolved.
9
+ class DeliveryJob < Monk::Job
10
+ queue "mailers"
11
+ never_retry PermanentDeliveryError
12
+
13
+ # The SMTP failures that are final, by class name so this file never
14
+ # loads net/smtp itself (the app's Gemfile decides that, ADR 0012):
15
+ # a 5xx reply, a malformed command, refused credentials. Everything
16
+ # else -- a 4xx "try later", a refused connection, TLS, a timeout --
17
+ # is worth retrying.
18
+ PERMANENT_SMTP_ERRORS = %w[Net::SMTPFatalError Net::SMTPSyntaxError Net::SMTPAuthenticationError].freeze
19
+
20
+ def self.perform(fields)
21
+ Monk::Mail.deliver(**fields.transform_keys(&:to_sym))
22
+ rescue DeliveryError => e
23
+ raise unless PERMANENT_SMTP_ERRORS.include?(e.cause&.class&.name)
24
+
25
+ raise PermanentDeliveryError, e.message
26
+ end
27
+ end
28
+
29
+ class << self
30
+ # deliver, later: the same arguments, plus enqueue's wait:, at: and
31
+ # conn: (the last one enqueues inside the caller's transaction, so the
32
+ # email commits or rolls back with the app's own writes). Returns the
33
+ # job's id. The message is built -- and so checked -- here, before
34
+ # anything is enqueued: bad input raises InvalidMessageError in the
35
+ # caller, exactly as deliver would, rather than failing later in the
36
+ # job process. Render HTML before calling (html: Monk::Mail.render(...)):
37
+ # the job stores the finished message and only sends it.
38
+ #
39
+ # Monk::Mail.deliver_later(to: order[:email], subject: "Your receipt", text: receipt_text)
40
+ def deliver_later(to:, subject:, text: nil, html: nil, reply_to: nil, from: nil, wait: nil, at: nil, conn: nil)
41
+ raise NotConfiguredError, "call Monk::Mail.configure(url:, from:) before Monk::Mail.deliver_later" unless config
42
+
43
+ from ||= config[:from]
44
+ if from.nil?
45
+ raise InvalidMessageError, "no from: given, and Monk::Mail.configure has no default from: (MAIL_FROM)"
46
+ end
47
+
48
+ message = Message.new(from: from, to: to, subject: subject, text: text, html: html, reply_to: reply_to)
49
+ DeliveryJob.enqueue(message.to_h.transform_keys(&:to_s), wait: wait, at: at, conn: conn)
50
+ end
51
+ end
52
+ end
53
+ end
@@ -1,3 +1,4 @@
1
+ require "json"
1
2
  require "pg"
2
3
 
3
4
  require_relative "../persistence"
@@ -16,12 +17,31 @@ module Monk
16
17
  module Pg
17
18
  extend Monk::Persistence::Registry
18
19
 
20
+ # json/jsonb columns, decoded with plain JSON.parse. pg's own
21
+ # PG::TextDecoder::JSON (1.6.3) calls JSON.parse(string, quirks_mode:
22
+ # true), and json 3 removed that keyword, so with pg's default every
23
+ # json/jsonb read raised ArgumentError. json 3 parses a bare scalar
24
+ # ("7", "\"text\"") without it.
25
+ class JsonDecoder < PG::SimpleDecoder
26
+ def decode(string, _tuple = nil, _field = nil)
27
+ JSON.parse(string)
28
+ end
29
+ end
30
+
31
+ # pg's default result types, with JsonDecoder for json and jsonb.
32
+ # Built once and made shareable, so every Ractor's #connect can use
33
+ # it rather than building its own.
34
+ RESULT_TYPES = Ractor.make_shareable(
35
+ PG::BasicTypeRegistry.new.register_default_types
36
+ .tap { |types| %w[json jsonb].each { |name| types.register_type(0, name, nil, JsonDecoder) } },
37
+ )
38
+
19
39
  class << self
20
40
  private
21
41
 
22
42
  def connect(**opts)
23
43
  conn = PG.connect(**opts)
24
- conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)
44
+ conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn, registry: RESULT_TYPES)
25
45
  conn
26
46
  end
27
47