monkrb 0.20.0 → 0.21.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -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 +4 -3
  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 +8 -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 +25 -0
  91. data/lib/monk/templates/websocket/app/sockets/chat.rb +18 -0
  92. data/lib/monk/templates/websocket/bin/websocket_server +58 -0
  93. data/lib/monk/templates/websocket/config/websocket_postgres.rb +24 -0
  94. data/lib/monk/templates/websocket/config/websocket_redis.rb +21 -0
  95. data/lib/monk/templates/websocket/setup.md +51 -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,25 @@
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
+ - With Postgres, a broadcast is capped at just under 8000 bytes (NOTIFY).
22
+ - `WS_ALLOWED_ORIGINS` must list your pages' origins.
23
+ - Switching transports is three steps in SETUP.md, websocket.
24
+
25
+ **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,58 @@
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
+ # This process holds the sockets, so it's the only one that listens for
29
+ # what the others publish: the transport's subscriber connection opens
30
+ # here, and an unreachable Postgres or Redis fails right here.
31
+ AppWebSocket::REGISTRY.listen!
32
+
33
+ # With auth, a socket carries its user's session. With live, a visitor's
34
+ # socket comes in anonymous instead of refused, and live's rules
35
+ # (config/live.rb) decide what it may subscribe to.
36
+ authenticate = if defined?(Monk::Auth) && Monk::Auth.config
37
+ live ? :optional : true
38
+ else
39
+ false
40
+ end
41
+ # Sessions are checked through config/auth.rb's :auth pool, not in each
42
+ # socket's own Ractor, which would hold a Postgres connection per open
43
+ # page. This is the only process that uses the pool, so it starts it.
44
+ db_pool = (:auth if authenticate)
45
+ Monk::Persistence::Pg.start_pools!(db_pool) if db_pool
46
+
47
+ port = ENV.fetch("WS_PORT", "9293").to_i
48
+ # Defaults to public_url (config/settings.rb): one PUBLIC_URL keeps both
49
+ # in sync. More than one origin: comma-separated.
50
+ allowed_origins = ENV.fetch("WS_ALLOWED_ORIGINS", Monk::Settings[:public_url]).split(",")
51
+
52
+ server = Monk::WebSocket::Server.new(
53
+ port: port, authenticate: authenticate, allowed_origins: allowed_origins, db_pool: db_pool,
54
+ )
55
+ puts "Monk::WebSocket::Server listening on ws://0.0.0.0:#{port} " \
56
+ "(#{live ? "live" : "app/sockets"}, transport: #{AppWebSocket::TRANSPORT}, authenticate: #{authenticate})"
57
+ server.run(&handler)
58
+ end
@@ -0,0 +1,24 @@
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, over the
10
+ # :primary connection's settings (config/persistence.rb). A broadcast is
11
+ # capped at just under 8000 bytes, Postgres's NOTIFY limit. To switch to
12
+ # Redis: SETUP.md, websocket.
13
+ module AppWebSocket
14
+ TRANSPORT = "postgres".freeze
15
+
16
+ # A registry over this app's transport. bin/websocket_server's (REGISTRY)
17
+ # also listens; every other process only publishes.
18
+ def self.build_registry
19
+ Monk::WebSocket::PgFanout.new(Monk::WebSocket::Registry.new,
20
+ pg_opts: Monk::Persistence::Pg.connection_options(:primary),)
21
+ end
22
+
23
+ REGISTRY = build_registry
24
+ end
@@ -0,0 +1,21 @@
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
+ REGISTRY = build_registry
21
+ end
@@ -0,0 +1,51 @@
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. In `config/websocket.rb`, swap the fanout: `Monk::WebSocket::RedisFanout`
28
+ with `redis_url: Monk::Settings[:redis_url]` (and `require_relative
29
+ "redis"`), or `Monk::WebSocket::PgFanout` with
30
+ `pg_opts: Monk::Persistence::Pg.connection_options(:primary)` (and
31
+ `require_relative "persistence"`).
32
+ 3. Restart `bin/server`, `bin/websocket_server` and `bin/jobs`.
33
+
34
+ ### Check it works
35
+
36
+ 1. `bundle exec rake test` runs `test/websocket_test.rb`: a broadcast
37
+ crosses from one registry to another over the transport.
38
+ 2. By hand, without live: uncomment the `websocket-chat` block in
39
+ `app/sockets/chat.rb` and start `bin/websocket_server` next to
40
+ `bin/server`. Open http://localhost:9292 in two tabs, and in each
41
+ browser console:
42
+
43
+ ```js
44
+ ws = new WebSocket("ws://localhost:9293")
45
+ ws.onmessage = (event) => console.log(event.data)
46
+ ```
47
+
48
+ `ws.send("hi")` in one tab logs `guest: hi` in both. With auth, the
49
+ server lets only logged-in sockets in: log in first (auth, above), and
50
+ the message carries your email. With live, `bin/websocket_server` runs
51
+ 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.0".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
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: monkrb
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.0
4
+ version: 0.21.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matteo Folin
@@ -93,6 +93,20 @@ dependencies:
93
93
  - - "~>"
94
94
  - !ruby/object:Gem::Version
95
95
  version: '0.5'
96
+ - !ruby/object:Gem::Dependency
97
+ name: dotenv
98
+ requirement: !ruby/object:Gem::Requirement
99
+ requirements:
100
+ - - "~>"
101
+ - !ruby/object:Gem::Version
102
+ version: '3.0'
103
+ type: :development
104
+ prerelease: false
105
+ version_requirements: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - "~>"
108
+ - !ruby/object:Gem::Version
109
+ version: '3.0'
96
110
  - !ruby/object:Gem::Dependency
97
111
  name: minitest
98
112
  requirement: !ruby/object:Gem::Requirement
@@ -170,12 +184,28 @@ files:
170
184
  - lib/monk/auth/helpers.rb
171
185
  - lib/monk/auth/login_token.rb
172
186
  - lib/monk/auth/rate_limiter.rb
187
+ - lib/monk/auth/send_login_link.rb
173
188
  - lib/monk/auth/session.rb
174
189
  - lib/monk/base.rb
190
+ - lib/monk/bin.rb
191
+ - lib/monk/cli.rb
175
192
  - lib/monk/context.rb
176
193
  - lib/monk/environment.rb
177
194
  - lib/monk/errors.rb
178
195
  - lib/monk/freeze_hooks.rb
196
+ - lib/monk/generator.rb
197
+ - lib/monk/generator/definition.rb
198
+ - lib/monk/generator/result.rb
199
+ - lib/monk/generator/run.rb
200
+ - lib/monk/generators.rb
201
+ - lib/monk/generators/auth.rb
202
+ - lib/monk/generators/base.rb
203
+ - lib/monk/generators/jobs.rb
204
+ - lib/monk/generators/live.rb
205
+ - lib/monk/generators/mail.rb
206
+ - lib/monk/generators/postgres.rb
207
+ - lib/monk/generators/redis.rb
208
+ - lib/monk/generators/websocket.rb
179
209
  - lib/monk/jobs.rb
180
210
  - lib/monk/jobs/adapters/pg.rb
181
211
  - lib/monk/jobs/args.rb
@@ -199,6 +229,7 @@ files:
199
229
  - lib/monk/log.rb
200
230
  - lib/monk/mail.rb
201
231
  - lib/monk/mail/address.rb
232
+ - lib/monk/mail/deliver_later.rb
202
233
  - lib/monk/mail/errors.rb
203
234
  - lib/monk/mail/later.rb
204
235
  - lib/monk/mail/message.rb
@@ -213,19 +244,26 @@ files:
213
244
  - lib/monk/persistence/pg/migrator.rb
214
245
  - lib/monk/persistence/pg/model.rb
215
246
  - lib/monk/persistence/pool.rb
216
- - lib/monk/scaffold.rb
217
247
  - lib/monk/settings.rb
218
248
  - lib/monk/state_ractor.rb
219
- - lib/monk/templates/auth/app/mailers/app_mailer.rb
249
+ - lib/monk/templates/auth/agents.md
250
+ - lib/monk/templates/auth/app/mailers/auth_mailer.rb
251
+ - lib/monk/templates/auth/app/routes/auth.rb
252
+ - lib/monk/templates/auth/app/views/auth/login.erb
220
253
  - lib/monk/templates/auth/app/views/mail/magic_link.erb
221
254
  - lib/monk/templates/auth/config/auth.rb
222
255
  - lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.down.sql
223
256
  - lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.up.sql
257
+ - lib/monk/templates/auth/setup.md
258
+ - lib/monk/templates/auth/test/auth_test.rb
224
259
  - lib/monk/templates/base/.dockerignore
225
260
  - lib/monk/templates/base/.gitignore
226
261
  - lib/monk/templates/base/.ruby-version
262
+ - lib/monk/templates/base/CLAUDE.md
227
263
  - lib/monk/templates/base/Dockerfile
228
264
  - lib/monk/templates/base/Gemfile
265
+ - lib/monk/templates/base/Rakefile
266
+ - lib/monk/templates/base/agents.md
229
267
  - lib/monk/templates/base/app/app.rb
230
268
  - lib/monk/templates/base/app/broadcasts/.keep
231
269
  - lib/monk/templates/base/app/helpers/.keep
@@ -233,37 +271,66 @@ files:
233
271
  - lib/monk/templates/base/app/mailers/.keep
234
272
  - lib/monk/templates/base/app/models/.keep
235
273
  - lib/monk/templates/base/app/presenters/.keep
274
+ - lib/monk/templates/base/app/routes/.keep
236
275
  - lib/monk/templates/base/app/views/index.erb
237
276
  - lib/monk/templates/base/app/views/layouts/app.erb
238
277
  - lib/monk/templates/base/bin/server
239
- - lib/monk/templates/base/bin/websocket_server
240
278
  - lib/monk/templates/base/config.ru
241
279
  - lib/monk/templates/base/config/load.rb
242
280
  - lib/monk/templates/base/config/settings.rb
243
281
  - lib/monk/templates/base/public/css/app.css
244
282
  - lib/monk/templates/base/public/js/app.js
283
+ - lib/monk/templates/base/setup.md
284
+ - lib/monk/templates/base/test/app_test.rb
285
+ - lib/monk/templates/base/test/test_helper.rb
286
+ - lib/monk/templates/jobs/agents.md
245
287
  - lib/monk/templates/jobs/app/jobs/hello_job.rb
246
- - lib/monk/templates/jobs/app/jobs/send_login_link.rb
288
+ - lib/monk/templates/jobs/app/routes/jobs.rb
247
289
  - lib/monk/templates/jobs/bin/jobs
248
290
  - lib/monk/templates/jobs/config/jobs.rb
249
291
  - lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.down.sql
250
292
  - lib/monk/templates/jobs/db/migrate/00000000000002_create_jobs_tables.up.sql
251
- - lib/monk/templates/live/app/app.rb
252
- - lib/monk/templates/live/app/views/index.erb
253
- - lib/monk/templates/live/app/views/live/_hits.erb
254
- - lib/monk/templates/live/bin/websocket_server
293
+ - lib/monk/templates/jobs/setup.md
294
+ - lib/monk/templates/jobs/test/jobs_test.rb
295
+ - lib/monk/templates/live/agents.md
296
+ - lib/monk/templates/live/app/broadcasts/greeting.rb
297
+ - lib/monk/templates/live/app/routes/demo_live.rb
298
+ - lib/monk/templates/live/app/routes/live.rb
299
+ - lib/monk/templates/live/app/views/demo/_hits.erb
300
+ - lib/monk/templates/live/app/views/demo/live.erb
301
+ - lib/monk/templates/live/app/views/live/_greeting.erb
255
302
  - lib/monk/templates/live/config/live.rb
256
- - lib/monk/templates/live/config/live_pg.rb
257
- - lib/monk/templates/mail/Gemfile.extra
303
+ - lib/monk/templates/live/config/live_without_demo.rb
304
+ - lib/monk/templates/live/setup.md
305
+ - lib/monk/templates/live/test/live_test.rb
306
+ - lib/monk/templates/mail/agents.md
307
+ - lib/monk/templates/mail/app/mailers/app_mailer.rb
308
+ - lib/monk/templates/mail/app/routes/mail.rb
309
+ - lib/monk/templates/mail/app/views/mail/welcome.erb
258
310
  - lib/monk/templates/mail/config/mail.rb
259
- - lib/monk/templates/postgres/Dockerfile
260
- - lib/monk/templates/postgres/Gemfile.extra
311
+ - lib/monk/templates/mail/setup.md
312
+ - lib/monk/templates/mail/test/mail_test.rb
313
+ - lib/monk/templates/postgres/agents.md
314
+ - lib/monk/templates/postgres/app/routes/postgres.rb
261
315
  - lib/monk/templates/postgres/bin/console
262
316
  - lib/monk/templates/postgres/bin/migrate
263
317
  - lib/monk/templates/postgres/bin/setup_db
264
318
  - lib/monk/templates/postgres/config/persistence.rb
265
319
  - lib/monk/templates/postgres/db/migrate/.keep
266
- - lib/monk/templates/redis/Gemfile.extra
320
+ - lib/monk/templates/postgres/setup.md
321
+ - lib/monk/templates/postgres/test/persistence_test.rb
322
+ - lib/monk/templates/redis/agents.md
323
+ - lib/monk/templates/redis/app/routes/redis.rb
324
+ - lib/monk/templates/redis/config/redis.rb
325
+ - lib/monk/templates/redis/setup.md
326
+ - lib/monk/templates/redis/test/redis_test.rb
327
+ - lib/monk/templates/websocket/agents.md
328
+ - lib/monk/templates/websocket/app/sockets/chat.rb
329
+ - lib/monk/templates/websocket/bin/websocket_server
330
+ - lib/monk/templates/websocket/config/websocket_postgres.rb
331
+ - lib/monk/templates/websocket/config/websocket_redis.rb
332
+ - lib/monk/templates/websocket/setup.md
333
+ - lib/monk/templates/websocket/test/websocket_test.rb
267
334
  - lib/monk/version.rb
268
335
  - lib/monk/views.rb
269
336
  - lib/monk/websocket.rb