monkrb 0.20.0 → 0.21.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.
Files changed (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/README.md +7 -6
  4. data/exe/monk +3 -181
  5. data/lib/monk/assets.rb +39 -9
  6. data/lib/monk/auth/errors.rb +3 -0
  7. data/lib/monk/auth/helpers.rb +11 -0
  8. data/lib/monk/auth/send_login_link.rb +36 -0
  9. data/lib/monk/auth.rb +34 -5
  10. data/lib/monk/bin.rb +107 -0
  11. data/lib/monk/cli.rb +277 -0
  12. data/lib/monk/context.rb +17 -0
  13. data/lib/monk/errors.rb +0 -6
  14. data/lib/monk/generator/definition.rb +153 -0
  15. data/lib/monk/generator/result.rb +279 -0
  16. data/lib/monk/generator/run.rb +449 -0
  17. data/lib/monk/generator.rb +48 -0
  18. data/lib/monk/generators/auth.rb +29 -0
  19. data/lib/monk/generators/base.rb +19 -0
  20. data/lib/monk/generators/jobs.rb +21 -0
  21. data/lib/monk/generators/live.rb +26 -0
  22. data/lib/monk/generators/mail.rb +24 -0
  23. data/lib/monk/generators/postgres.rb +23 -0
  24. data/lib/monk/generators/redis.rb +20 -0
  25. data/lib/monk/generators/websocket.rb +24 -0
  26. data/lib/monk/generators.rb +7 -0
  27. data/lib/monk/jobs.rb +7 -0
  28. data/lib/monk/live/helpers.rb +13 -0
  29. data/lib/monk/live.rb +12 -1
  30. data/lib/monk/log.rb +10 -1
  31. data/lib/monk/mail/deliver_later.rb +54 -0
  32. data/lib/monk/mail/later.rb +3 -51
  33. data/lib/monk/mail.rb +4 -0
  34. data/lib/monk/templates/auth/agents.md +29 -0
  35. data/lib/monk/templates/auth/app/mailers/{app_mailer.rb → auth_mailer.rb} +5 -5
  36. data/lib/monk/templates/auth/app/routes/auth.rb +48 -0
  37. data/lib/monk/templates/auth/app/views/auth/login.erb +25 -0
  38. data/lib/monk/templates/auth/config/auth.rb +8 -8
  39. data/lib/monk/templates/auth/setup.md +37 -0
  40. data/lib/monk/templates/auth/test/auth_test.rb +17 -0
  41. data/lib/monk/templates/base/CLAUDE.md +1 -0
  42. data/lib/monk/templates/base/Dockerfile +10 -5
  43. data/lib/monk/templates/base/Gemfile +7 -2
  44. data/lib/monk/templates/base/Rakefile +8 -0
  45. data/lib/monk/templates/base/agents.md +41 -0
  46. data/lib/monk/templates/base/app/app.rb +5 -0
  47. data/lib/monk/templates/base/app/routes/.keep +0 -0
  48. data/lib/monk/templates/base/app/views/index.erb +1 -1
  49. data/lib/monk/templates/base/app/views/layouts/app.erb +2 -0
  50. data/lib/monk/templates/base/config/load.rb +14 -0
  51. data/lib/monk/templates/base/config/settings.rb +4 -5
  52. data/lib/monk/templates/base/setup.md +22 -0
  53. data/lib/monk/templates/base/test/app_test.rb +12 -0
  54. data/lib/monk/templates/base/test/test_helper.rb +18 -0
  55. data/lib/monk/templates/jobs/agents.md +25 -0
  56. data/lib/monk/templates/jobs/app/routes/jobs.rb +9 -0
  57. data/lib/monk/templates/jobs/bin/jobs +19 -13
  58. data/lib/monk/templates/jobs/setup.md +30 -0
  59. data/lib/monk/templates/jobs/test/jobs_test.rb +16 -0
  60. data/lib/monk/templates/live/agents.md +25 -0
  61. data/lib/monk/templates/live/app/broadcasts/greeting.rb +13 -0
  62. data/lib/monk/templates/live/app/routes/demo_live.rb +21 -0
  63. data/lib/monk/templates/live/app/routes/live.rb +10 -0
  64. data/lib/monk/templates/live/app/views/demo/live.erb +18 -0
  65. data/lib/monk/templates/live/app/views/live/_greeting.erb +1 -0
  66. data/lib/monk/templates/live/config/live.rb +30 -32
  67. data/lib/monk/templates/live/config/live_without_demo.rb +38 -0
  68. data/lib/monk/templates/live/setup.md +39 -0
  69. data/lib/monk/templates/live/test/live_test.rb +22 -0
  70. data/lib/monk/templates/mail/agents.md +22 -0
  71. data/lib/monk/templates/mail/app/mailers/app_mailer.rb +14 -0
  72. data/lib/monk/templates/mail/app/routes/mail.rb +12 -0
  73. data/lib/monk/templates/mail/app/views/mail/welcome.erb +1 -0
  74. data/lib/monk/templates/mail/config/mail.rb +4 -4
  75. data/lib/monk/templates/mail/setup.md +32 -0
  76. data/lib/monk/templates/mail/test/mail_test.rb +13 -0
  77. data/lib/monk/templates/postgres/agents.md +25 -0
  78. data/lib/monk/templates/postgres/app/routes/postgres.rb +22 -0
  79. data/lib/monk/templates/postgres/bin/console +3 -1
  80. data/lib/monk/templates/postgres/bin/migrate +23 -17
  81. data/lib/monk/templates/postgres/bin/setup_db +11 -5
  82. data/lib/monk/templates/postgres/config/persistence.rb +1 -0
  83. data/lib/monk/templates/postgres/setup.md +38 -0
  84. data/lib/monk/templates/postgres/test/persistence_test.rb +11 -0
  85. data/lib/monk/templates/redis/agents.md +18 -0
  86. data/lib/monk/templates/redis/app/routes/redis.rb +15 -0
  87. data/lib/monk/templates/redis/config/redis.rb +13 -0
  88. data/lib/monk/templates/redis/setup.md +20 -0
  89. data/lib/monk/templates/redis/test/redis_test.rb +12 -0
  90. data/lib/monk/templates/websocket/agents.md +28 -0
  91. data/lib/monk/templates/websocket/app/sockets/chat.rb +18 -0
  92. data/lib/monk/templates/websocket/bin/websocket_server +62 -0
  93. data/lib/monk/templates/websocket/config/websocket_postgres.rb +38 -0
  94. data/lib/monk/templates/websocket/config/websocket_redis.rb +26 -0
  95. data/lib/monk/templates/websocket/setup.md +54 -0
  96. data/lib/monk/templates/websocket/test/websocket_test.rb +21 -0
  97. data/lib/monk/version.rb +1 -1
  98. data/lib/monk/websocket/server.rb +3 -1
  99. metadata +81 -14
  100. data/lib/monk/scaffold.rb +0 -1063
  101. data/lib/monk/templates/base/bin/websocket_server +0 -77
  102. data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +0 -28
  103. data/lib/monk/templates/live/app/app.rb +0 -22
  104. data/lib/monk/templates/live/app/views/index.erb +0 -18
  105. data/lib/monk/templates/live/bin/websocket_server +0 -50
  106. data/lib/monk/templates/live/config/live_pg.rb +0 -60
  107. data/lib/monk/templates/mail/Gemfile.extra +0 -1
  108. data/lib/monk/templates/postgres/Dockerfile +0 -32
  109. data/lib/monk/templates/postgres/Gemfile.extra +0 -2
  110. data/lib/monk/templates/redis/Gemfile.extra +0 -1
  111. /data/lib/monk/templates/live/app/views/{live → demo}/_hits.erb +0 -0
@@ -1,22 +1,28 @@
1
1
  #!/usr/bin/env ruby
2
2
  require "bundler/setup"
3
- require_relative "../config/settings"
4
- require_relative "../config/persistence"
5
- require "monk/persistence/pg/migrator"
3
+ # A service that's down, a missing database or setting ends this script
4
+ # with what to fix (SETUP.md's commands), not a backtrace.
5
+ require "monk/bin"
6
6
 
7
- migrator = Monk::Persistence::Pg::Migrator.new(db_name: :primary, dir: File.expand_path("../db/migrate", __dir__))
7
+ Monk::Bin.run(__FILE__) do
8
+ require_relative "../config/settings"
9
+ require_relative "../config/persistence"
10
+ require "monk/persistence/pg/migrator"
8
11
 
9
- case ARGV[0]
10
- when nil, "migrate"
11
- applied = migrator.migrate!
12
- puts applied.empty? ? "Nothing to migrate." : "Applied: #{applied.join(", ")}"
13
- when "rollback"
14
- reverted = migrator.rollback!(steps: (ARGV[1] || 1).to_i)
15
- puts reverted.empty? ? "Nothing to roll back." : "Reverted: #{reverted.join(", ")}"
16
- when "status"
17
- puts "Applied: #{migrator.applied.join(", ")}"
18
- puts "Pending: #{migrator.pending.join(", ")}"
19
- else
20
- warn "Usage: bin/migrate [migrate|rollback [N]|status]"
21
- exit 1
12
+ migrator = Monk::Persistence::Pg::Migrator.new(db_name: :primary, dir: File.expand_path("../db/migrate", __dir__))
13
+
14
+ case ARGV[0]
15
+ when nil, "migrate"
16
+ applied = migrator.migrate!
17
+ puts applied.empty? ? "Nothing to migrate." : "Applied: #{applied.join(", ")}"
18
+ when "rollback"
19
+ reverted = migrator.rollback!(steps: (ARGV[1] || 1).to_i)
20
+ puts reverted.empty? ? "Nothing to roll back." : "Reverted: #{reverted.join(", ")}"
21
+ when "status"
22
+ puts "Applied: #{migrator.applied.join(", ")}"
23
+ puts "Pending: #{migrator.pending.join(", ")}"
24
+ else
25
+ warn "Usage: bin/migrate [migrate|rollback [N]|status]"
26
+ exit 1
27
+ end
22
28
  end
@@ -1,9 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
2
  require "bundler/setup"
3
- require_relative "../config/settings"
4
- require_relative "../config/persistence"
5
- require "monk/persistence/pg/migrator"
3
+ # A service that's down, a missing database or setting ends this script
4
+ # with what to fix (SETUP.md's commands), not a backtrace.
5
+ require "monk/bin"
6
6
 
7
- Monk::Persistence::Pg::Migrator.new(db_name: :primary, dir: File.expand_path("../db/migrate", __dir__)).migrate!
7
+ Monk::Bin.run(__FILE__) do
8
+ require_relative "../config/settings"
9
+ require_relative "../config/persistence"
10
+ require "monk/persistence/pg/migrator"
8
11
 
9
- puts "Migrated #{ENV.fetch("DB_NAME", "app_development")}."
12
+ Monk::Persistence::Pg::Migrator.new(db_name: :primary, dir: File.expand_path("../db/migrate", __dir__)).migrate!
13
+
14
+ puts "Migrated #{ENV.fetch("DB_NAME", "app_development")}."
15
+ end
@@ -1,5 +1,6 @@
1
1
  require "monk"
2
2
  require "monk/persistence/pg"
3
+ require "monk/persistence/pg/model" # Monk::Persistence::Pg::Model, for app/models/
3
4
 
4
5
  Monk::Persistence::Pg.register(:primary,
5
6
  host: ENV.fetch("DB_HOST", "127.0.0.1"),
@@ -0,0 +1,38 @@
1
+ ## postgres
2
+
3
+ `.env` and `.env.test` point at a local Postgres (`DB_*`), with database
4
+ names taken from this app's directory: `{{app}}_development` and
5
+ `{{app}}_test`. `MONK_ENV=test` doesn't change the database: only `DB_NAME`
6
+ does, and `test/test_helper.rb` takes it from `.env.test`.
7
+
8
+ ### Start Postgres
9
+
10
+ Check first whether one is already running (`docker ps`): a second
11
+ container on the same port fails with "port is already allocated". Either
12
+ point `DB_HOST`/`DB_PORT`/`DB_USER`/`DB_PASSWORD` in `.env` and `.env.test`
13
+ at the running one, or start one:
14
+
15
+ ```bash
16
+ docker run --rm -d -p 5432:5432 -e POSTGRES_PASSWORD=postgres --name {{app}}_pg postgres:16
17
+ ```
18
+
19
+ ### Create the databases and migrate them
20
+
21
+ ```bash
22
+ PGPASSWORD=postgres createdb -h 127.0.0.1 -p 5432 -U postgres {{app}}_development
23
+ PGPASSWORD=postgres createdb -h 127.0.0.1 -p 5432 -U postgres {{app}}_test
24
+ bin/setup_db
25
+ DB_NAME={{app}}_test bin/setup_db
26
+ ```
27
+
28
+ `createdb` without `-h` uses a Unix socket, which a container doesn't
29
+ provide. Run both `bin/setup_db` lines again after adding a migration.
30
+
31
+ ### Check it works
32
+
33
+ 1. `bundle exec rake test` runs `test/persistence_test.rb`, which connects
34
+ to the test database.
35
+ 2. By hand: `bin/console` opens IRB with the app's configs and models
36
+ loaded; `Monk::Persistence::Pg.checkout(:primary) { |conn| conn.exec("SELECT 1").values }`
37
+ answers `[[1]]`. `bin/migrate status` lists applied and pending
38
+ migrations.
@@ -0,0 +1,11 @@
1
+ require_relative "test_helper"
2
+
3
+ # The test database is reachable with .env.test's settings, and migrated
4
+ # (SETUP.md, postgres).
5
+ class PersistenceTest < Minitest::Test
6
+ def test_connects_to_the_test_database
7
+ Monk::Persistence::Pg.checkout(:primary) do |conn|
8
+ assert_equal 1, conn.exec("SELECT 1").getvalue(0, 0)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ ## redis
2
+
3
+ **Where:** `config/redis.rb` declares the `redis_url` setting (from
4
+ `REDIS_URL`) and loads the `redis` gem.
5
+
6
+ **Main calls:** `Redis.new(url: settings[:redis_url])` in a route (or
7
+ `Monk::Settings[:redis_url]` elsewhere), then the redis gem's commands:
8
+ `get`, `set(key, value, ex: seconds)`, `incr`, `del`. Close it when done.
9
+
10
+ **Test:** `test/redis_test.rb` pings Redis at `.env.test`'s `REDIS_URL`.
11
+
12
+ **Pitfalls:**
13
+ - A Redis client isn't Ractor-shareable: never keep one in a constant or a
14
+ module variable. Open it in the route (or job) that uses it.
15
+ - Monk has no Redis pool: each `Redis.new` is a connection. Close it.
16
+ - `REDIS_URL` is required outside development and test.
17
+
18
+ **Examples:** `redis-cache` in `app/routes/redis.rb`.
@@ -0,0 +1,15 @@
1
+ # Routes that use the app's Redis (monk add redis).
2
+ #
3
+ # monk:example redis-cache -- a value computed once a minute, cached in Redis.
4
+ # # A Redis client can't be shared between Ractors, and routes run in them,
5
+ # # so each request opens its own; Monk has no Redis pool yet.
6
+ # class App
7
+ # get("/cached/now") do
8
+ # redis = Redis.new(url: settings[:redis_url])
9
+ # now = redis.get("cached:now") || Time.now.to_i.to_s.tap { |value| redis.set("cached:now", value, ex: 60) }
10
+ # json(now: now)
11
+ # ensure
12
+ # redis&.close
13
+ # end
14
+ # end
15
+ # monk:end
@@ -0,0 +1,13 @@
1
+ require "monk"
2
+ require "redis"
3
+
4
+ # Where this app's Redis is (monk add redis): for a cache, rate limits,
5
+ # locks, or as WebSocket's transport (config/websocket.rb). A local one by
6
+ # default in development and test; anywhere else REDIS_URL must be set.
7
+ Monk::Settings.configure do
8
+ if Monk.env.development? || Monk.env.test?
9
+ optional :redis_url, default: "redis://localhost:6379/0"
10
+ else
11
+ required :redis_url
12
+ end
13
+ end
@@ -0,0 +1,20 @@
1
+ ## redis
2
+
3
+ `REDIS_URL` in `.env` and `.env.test` points at a local Redis
4
+ (`redis://localhost:6379/0`). Outside development and test it has no
5
+ default: set it, or the app fails at boot.
6
+
7
+ ### Start Redis
8
+
9
+ Check first whether one is already running (`docker ps`). Either point
10
+ `REDIS_URL` at it, or start one:
11
+
12
+ ```bash
13
+ docker run --rm -d -p 6379:6379 --name {{app}}_redis redis:7
14
+ ```
15
+
16
+ ### Check it works
17
+
18
+ 1. `bundle exec rake test` runs `test/redis_test.rb`, which pings it.
19
+ 2. By hand, in `bin/console`: `Redis.new(url: Monk::Settings[:redis_url]).ping`
20
+ answers `"PONG"`.
@@ -0,0 +1,12 @@
1
+ require_relative "test_helper"
2
+
3
+ # Redis is reachable at REDIS_URL (.env.test): SETUP.md, redis.
4
+ class RedisTest < Minitest::Test
5
+ def test_redis_answers
6
+ redis = Redis.new(url: Monk::Settings[:redis_url])
7
+
8
+ assert_equal "PONG", redis.ping
9
+ ensure
10
+ redis&.close
11
+ end
12
+ end
@@ -0,0 +1,28 @@
1
+ ## websocket
2
+
3
+ **Where:** `config/websocket.rb` (`AppWebSocket::REGISTRY`, over the app's
4
+ transport: `AppWebSocket::TRANSPORT`), `bin/websocket_server` (the socket
5
+ process), `app/sockets/` (handlers, when the app doesn't use live).
6
+
7
+ **Main calls:** `AppWebSocket::REGISTRY.broadcast(topic, payload)` from any
8
+ process. In a handler, `proc { |connection| ... }`: `connection.read`,
9
+ `connection.subscribe(AppWebSocket::REGISTRY, topic)`,
10
+ `connection.subject` (the user, with auth). The server runs
11
+ `AppSockets::HANDLER`, or live's.
12
+
13
+ **Test:** `test/websocket_test.rb` sends a broadcast across two registries
14
+ over the transport.
15
+
16
+ **Pitfalls:**
17
+ - Handlers run in each connection's own Ractor: build them in a module
18
+ body, never at a file's top level.
19
+ - Only `bin/websocket_server` calls `listen!`; every other process only
20
+ publishes.
21
+ - A process publishes only once `AppWebSocket.start!` has run:
22
+ `config/load.rb` calls it, so a script that loads `config/websocket.rb`
23
+ without it calls it itself, at boot, in the main Ractor.
24
+ - With Postgres, a broadcast is capped at just under 8000 bytes (NOTIFY).
25
+ - `WS_ALLOWED_ORIGINS` must list your pages' origins.
26
+ - Switching transports is three steps in SETUP.md, websocket.
27
+
28
+ **Examples:** `websocket-chat` in `app/sockets/chat.rb`.
@@ -0,0 +1,18 @@
1
+ # Socket handlers for bin/websocket_server, in an app without Monk::Live
2
+ # (with live, the server runs live's handler instead).
3
+ #
4
+ # monk:example websocket-chat -- one chat room: what a socket sends goes to every socket.
5
+ # # The handler runs in each connection's own Ractor, so it's built in a
6
+ # # module body, where self is shareable.
7
+ # module AppSockets
8
+ # HANDLER = proc do |connection|
9
+ # connection.subscribe(AppWebSocket::REGISTRY, :chat)
10
+ # loop do
11
+ # message = connection.read
12
+ # break unless message
13
+ #
14
+ # AppWebSocket::REGISTRY.broadcast(:chat, "#{connection.subject || "guest"}: #{message}")
15
+ # end
16
+ # end
17
+ # end
18
+ # monk:end
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env ruby
2
+ require "bundler/setup"
3
+ # A service that's down, a missing database or setting ends this script
4
+ # with what to fix (SETUP.md's commands), not a backtrace.
5
+ require "monk/bin"
6
+
7
+ Monk::Bin.run(__FILE__) do
8
+ require_relative "../config/settings"
9
+ require_relative "../config/websocket"
10
+
11
+ # Each only if this app has it: auth says whose a socket is, live what it
12
+ # may subscribe to.
13
+ %w[auth live].each do |name|
14
+ path = File.expand_path("../config/#{name}.rb", __dir__)
15
+ require path if File.exist?(path)
16
+ end
17
+ live = defined?(Monk::Live) && Monk::Live.registry
18
+
19
+ # Without live, the app's own handler: AppSockets::HANDLER, in app/sockets/.
20
+ Dir[File.expand_path("../app/sockets/*.rb", __dir__)].sort.each { |file| require file }
21
+ handler = live ? Monk::Live::HANDLER : (AppSockets::HANDLER if defined?(AppSockets::HANDLER))
22
+ unless handler
23
+ $stderr.puts "bin/websocket_server: nothing to serve. Add live (monk add live), or define " \
24
+ "AppSockets::HANDLER in app/sockets/ -- the websocket-chat example in app/sockets/chat.rb is one."
25
+ exit 1
26
+ end
27
+
28
+ # A socket's handler can broadcast too (the websocket-chat example does),
29
+ # so this process publishes like the others.
30
+ AppWebSocket.start!
31
+
32
+ # This process holds the sockets, so it's the only one that listens for
33
+ # what the others publish: the transport's subscriber connection opens
34
+ # here, and an unreachable Postgres or Redis fails right here.
35
+ AppWebSocket::REGISTRY.listen!
36
+
37
+ # With auth, a socket carries its user's session. With live, a visitor's
38
+ # socket comes in anonymous instead of refused, and live's rules
39
+ # (config/live.rb) decide what it may subscribe to.
40
+ authenticate = if defined?(Monk::Auth) && Monk::Auth.config
41
+ live ? :optional : true
42
+ else
43
+ false
44
+ end
45
+ # Sessions are checked through config/auth.rb's :auth pool, not in each
46
+ # socket's own Ractor, which would hold a Postgres connection per open
47
+ # page. This is the only process that uses the pool, so it starts it.
48
+ db_pool = (:auth if authenticate)
49
+ Monk::Persistence::Pg.start_pools!(db_pool) if db_pool
50
+
51
+ port = ENV.fetch("WS_PORT", "9293").to_i
52
+ # Defaults to public_url (config/settings.rb): one PUBLIC_URL keeps both
53
+ # in sync. More than one origin: comma-separated.
54
+ allowed_origins = ENV.fetch("WS_ALLOWED_ORIGINS", Monk::Settings[:public_url]).split(",")
55
+
56
+ server = Monk::WebSocket::Server.new(
57
+ port: port, authenticate: authenticate, allowed_origins: allowed_origins, db_pool: db_pool,
58
+ )
59
+ puts "Monk::WebSocket::Server listening on ws://0.0.0.0:#{port} " \
60
+ "(#{live ? "live" : "app/sockets"}, transport: #{AppWebSocket::TRANSPORT}, authenticate: #{authenticate})"
61
+ server.run(&handler)
62
+ end
@@ -0,0 +1,38 @@
1
+ require "monk"
2
+ require "monk/websocket"
3
+ require "monk/websocket/pg_fanout"
4
+ require_relative "persistence"
5
+
6
+ # The WebSocket registry (monk add websocket): which sockets are subscribed
7
+ # to which topics, and how a broadcast reaches them. bin/server, bin/jobs
8
+ # and bin/websocket_server are separate processes, so Postgres
9
+ # LISTEN/NOTIFY carries a broadcast from one to the others, on the
10
+ # :primary database (config/persistence.rb). A broadcast is capped at just
11
+ # under 8000 bytes, Postgres's NOTIFY limit. To switch to Redis: SETUP.md,
12
+ # websocket.
13
+ #
14
+ # Broadcasts are published through the :notify pool: one connection per
15
+ # process, however many Ractors publish. Size 1, so a page's updates are
16
+ # sent in the order they were made. Declaring it opens nothing:
17
+ # AppWebSocket.start! does, in every process that publishes (config/load.rb
18
+ # and bin/websocket_server call it).
19
+ Monk::Persistence::Pg.pool(:notify, size: 1)
20
+
21
+ module AppWebSocket
22
+ TRANSPORT = "postgres".freeze
23
+
24
+ # A registry over this app's transport. bin/websocket_server's (REGISTRY)
25
+ # also listens, on a connection of its own; every other process only
26
+ # publishes.
27
+ def self.build_registry
28
+ Monk::WebSocket::PgFanout.new(Monk::WebSocket::Registry.new, db_pool: :notify)
29
+ end
30
+
31
+ # Opens the :notify pool's connection. At boot, in the main Ractor, once
32
+ # every database and pool is declared.
33
+ def self.start!
34
+ Monk::Persistence::Pg.start_pools!(:notify)
35
+ end
36
+
37
+ REGISTRY = build_registry
38
+ end
@@ -0,0 +1,26 @@
1
+ require "monk"
2
+ require "monk/websocket"
3
+ require "monk/websocket/redis_fanout"
4
+ require_relative "redis"
5
+
6
+ # The WebSocket registry (monk add websocket): which sockets are subscribed
7
+ # to which topics, and how a broadcast reaches them. bin/server, bin/jobs
8
+ # and bin/websocket_server are separate processes, so Redis carries a
9
+ # broadcast from one to the others, at REDIS_URL (config/redis.rb). To
10
+ # switch to Postgres: SETUP.md, websocket.
11
+ module AppWebSocket
12
+ TRANSPORT = "redis".freeze
13
+
14
+ # A registry over this app's transport. bin/websocket_server's (REGISTRY)
15
+ # also listens; every other process only publishes.
16
+ def self.build_registry
17
+ Monk::WebSocket::RedisFanout.new(Monk::WebSocket::Registry.new, redis_url: Monk::Settings[:redis_url])
18
+ end
19
+
20
+ # Nothing to open here: each publishing Ractor connects to Redis on its
21
+ # first broadcast. config/load.rb and bin/websocket_server call it all
22
+ # the same, as they would with Postgres.
23
+ def self.start!; end
24
+
25
+ REGISTRY = build_registry
26
+ end
@@ -0,0 +1,54 @@
1
+ ## websocket
2
+
3
+ `bin/websocket_server` holds the browsers' WebSocket connections, on :9293
4
+ (`WS_PORT`), next to `bin/server`. They're separate processes, so a
5
+ broadcast travels between them over this app's transport, chosen when the
6
+ module was added: `config/websocket.rb` says which.
7
+
8
+ ### Run it
9
+
10
+ ```bash
11
+ bin/websocket_server
12
+ ```
13
+
14
+ It serves Monk::Live (`monk add live`) or, without it, the handler in
15
+ `app/sockets/` (the `websocket-chat` example). With neither, it says so and
16
+ exits. In production it's one more process from the same image, with
17
+ `bin/websocket_server` as its command.
18
+
19
+ `WS_ALLOWED_ORIGINS` (default: `PUBLIC_URL`) must list every origin your
20
+ pages are served from, or browsers' sockets are refused. Behind TLS, pages
21
+ connect with `wss://`.
22
+
23
+ ### Switching transports
24
+
25
+ 1. If the other one isn't installed, add it: `monk add redis` or
26
+ `monk add postgres`.
27
+ 2. Replace `config/websocket.rb` with Monk's version for the other
28
+ transport (`redis` or `postgres`), then carry over anything you changed
29
+ in it:
30
+
31
+ ```bash
32
+ monk help websocket --file config/websocket.rb --transport=redis > config/websocket.rb
33
+ ```
34
+
35
+ 3. Restart `bin/server`, `bin/websocket_server` and `bin/jobs`.
36
+
37
+ ### Check it works
38
+
39
+ 1. `bundle exec rake test` runs `test/websocket_test.rb`: a broadcast
40
+ crosses from one registry to another over the transport.
41
+ 2. By hand, without live: uncomment the `websocket-chat` block in
42
+ `app/sockets/chat.rb` and start `bin/websocket_server` next to
43
+ `bin/server`. Open http://localhost:9292 in two tabs, and in each
44
+ browser console:
45
+
46
+ ```js
47
+ ws = new WebSocket("ws://localhost:9293")
48
+ ws.onmessage = (event) => console.log(event.data)
49
+ ```
50
+
51
+ `ws.send("hi")` in one tab logs `guest: hi` in both. With auth, the
52
+ server lets only logged-in sockets in: log in first (auth, above), and
53
+ the message carries your email. With live, `bin/websocket_server` runs
54
+ live's handler instead: live, below.
@@ -0,0 +1,21 @@
1
+ require_relative "test_helper"
2
+ require "timeout"
3
+
4
+ # A broadcast crosses processes over this app's transport
5
+ # (config/websocket.rb): a second registry, built the same way, publishes
6
+ # as bin/server would, and AppWebSocket::REGISTRY, listening as
7
+ # bin/websocket_server does, delivers it. SETUP.md, websocket.
8
+ class WebSocketTest < Minitest::Test
9
+ def test_a_broadcast_reaches_a_socket_through_the_transport
10
+ AppWebSocket::REGISTRY.listen!
11
+ port = Ractor::Port.new
12
+ AppWebSocket::REGISTRY.register(:websocket_check, port)
13
+
14
+ AppWebSocket.build_registry.broadcast(:websocket_check, "hello")
15
+
16
+ assert_equal "hello", Timeout.timeout(5) { port.receive }
17
+ ensure
18
+ AppWebSocket::REGISTRY.unregister(:websocket_check, port) if port
19
+ port&.close
20
+ end
21
+ end
data/lib/monk/version.rb CHANGED
@@ -5,5 +5,5 @@ module Monk
5
5
  # class of bug .freeze! guards against for routes/error handlers/models,
6
6
  # just on a top-level constant that every worker Ractor reads on boot
7
7
  # (found live under kino: GET /hello 500'd until this was frozen).
8
- VERSION = "0.20.0".freeze
8
+ VERSION = "0.21.1".freeze
9
9
  end
@@ -271,7 +271,9 @@ module Monk
271
271
  return true if credential&.fetch(:via) == :bearer
272
272
 
273
273
  origin = headers["origin"]
274
- allowed = credential ? origin && allowed_origins.include?(origin) : origin.nil? || allowed_origins.include?(origin)
274
+ # A cookie needs an allowed Origin; without one, only a non-browser
275
+ # client (no Origin at all) is let in.
276
+ allowed = origin.nil? ? !credential : allowed_origins.include?(origin)
275
277
  socket.write("HTTP/1.1 403 Forbidden\r\n\r\n") unless allowed
276
278
  allowed
277
279
  end