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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +77 -0
- data/README.md +7 -6
- data/exe/monk +3 -181
- data/lib/monk/assets.rb +39 -9
- data/lib/monk/auth/errors.rb +3 -0
- data/lib/monk/auth/helpers.rb +11 -0
- data/lib/monk/auth/send_login_link.rb +36 -0
- data/lib/monk/auth.rb +34 -5
- data/lib/monk/bin.rb +107 -0
- data/lib/monk/cli.rb +277 -0
- data/lib/monk/context.rb +17 -0
- data/lib/monk/errors.rb +0 -6
- data/lib/monk/generator/definition.rb +153 -0
- data/lib/monk/generator/result.rb +279 -0
- data/lib/monk/generator/run.rb +449 -0
- data/lib/monk/generator.rb +48 -0
- data/lib/monk/generators/auth.rb +29 -0
- data/lib/monk/generators/base.rb +19 -0
- data/lib/monk/generators/jobs.rb +21 -0
- data/lib/monk/generators/live.rb +26 -0
- data/lib/monk/generators/mail.rb +24 -0
- data/lib/monk/generators/postgres.rb +23 -0
- data/lib/monk/generators/redis.rb +20 -0
- data/lib/monk/generators/websocket.rb +24 -0
- data/lib/monk/generators.rb +7 -0
- data/lib/monk/jobs.rb +7 -0
- data/lib/monk/live/helpers.rb +13 -0
- data/lib/monk/live.rb +12 -1
- data/lib/monk/log.rb +10 -1
- data/lib/monk/mail/deliver_later.rb +54 -0
- data/lib/monk/mail/later.rb +3 -51
- data/lib/monk/mail.rb +4 -0
- data/lib/monk/templates/auth/agents.md +29 -0
- data/lib/monk/templates/auth/app/mailers/{app_mailer.rb → auth_mailer.rb} +5 -5
- data/lib/monk/templates/auth/app/routes/auth.rb +48 -0
- data/lib/monk/templates/auth/app/views/auth/login.erb +25 -0
- data/lib/monk/templates/auth/config/auth.rb +8 -8
- data/lib/monk/templates/auth/setup.md +37 -0
- data/lib/monk/templates/auth/test/auth_test.rb +17 -0
- data/lib/monk/templates/base/CLAUDE.md +1 -0
- data/lib/monk/templates/base/Dockerfile +4 -3
- data/lib/monk/templates/base/Gemfile +7 -2
- data/lib/monk/templates/base/Rakefile +8 -0
- data/lib/monk/templates/base/agents.md +41 -0
- data/lib/monk/templates/base/app/app.rb +5 -0
- data/lib/monk/templates/base/app/routes/.keep +0 -0
- data/lib/monk/templates/base/app/views/index.erb +1 -1
- data/lib/monk/templates/base/app/views/layouts/app.erb +2 -0
- data/lib/monk/templates/base/config/load.rb +8 -0
- data/lib/monk/templates/base/config/settings.rb +4 -5
- data/lib/monk/templates/base/setup.md +22 -0
- data/lib/monk/templates/base/test/app_test.rb +12 -0
- data/lib/monk/templates/base/test/test_helper.rb +18 -0
- data/lib/monk/templates/jobs/agents.md +25 -0
- data/lib/monk/templates/jobs/app/routes/jobs.rb +9 -0
- data/lib/monk/templates/jobs/bin/jobs +19 -13
- data/lib/monk/templates/jobs/setup.md +30 -0
- data/lib/monk/templates/jobs/test/jobs_test.rb +16 -0
- data/lib/monk/templates/live/agents.md +25 -0
- data/lib/monk/templates/live/app/broadcasts/greeting.rb +13 -0
- data/lib/monk/templates/live/app/routes/demo_live.rb +21 -0
- data/lib/monk/templates/live/app/routes/live.rb +10 -0
- data/lib/monk/templates/live/app/views/demo/live.erb +18 -0
- data/lib/monk/templates/live/app/views/live/_greeting.erb +1 -0
- data/lib/monk/templates/live/config/live.rb +30 -32
- data/lib/monk/templates/live/config/live_without_demo.rb +38 -0
- data/lib/monk/templates/live/setup.md +39 -0
- data/lib/monk/templates/live/test/live_test.rb +22 -0
- data/lib/monk/templates/mail/agents.md +22 -0
- data/lib/monk/templates/mail/app/mailers/app_mailer.rb +14 -0
- data/lib/monk/templates/mail/app/routes/mail.rb +12 -0
- data/lib/monk/templates/mail/app/views/mail/welcome.erb +1 -0
- data/lib/monk/templates/mail/config/mail.rb +4 -4
- data/lib/monk/templates/mail/setup.md +32 -0
- data/lib/monk/templates/mail/test/mail_test.rb +13 -0
- data/lib/monk/templates/postgres/agents.md +25 -0
- data/lib/monk/templates/postgres/app/routes/postgres.rb +22 -0
- data/lib/monk/templates/postgres/bin/console +3 -1
- data/lib/monk/templates/postgres/bin/migrate +23 -17
- data/lib/monk/templates/postgres/bin/setup_db +11 -5
- data/lib/monk/templates/postgres/config/persistence.rb +1 -0
- data/lib/monk/templates/postgres/setup.md +38 -0
- data/lib/monk/templates/postgres/test/persistence_test.rb +11 -0
- data/lib/monk/templates/redis/agents.md +18 -0
- data/lib/monk/templates/redis/app/routes/redis.rb +15 -0
- data/lib/monk/templates/redis/config/redis.rb +13 -0
- data/lib/monk/templates/redis/setup.md +20 -0
- data/lib/monk/templates/redis/test/redis_test.rb +12 -0
- data/lib/monk/templates/websocket/agents.md +25 -0
- data/lib/monk/templates/websocket/app/sockets/chat.rb +18 -0
- data/lib/monk/templates/websocket/bin/websocket_server +58 -0
- data/lib/monk/templates/websocket/config/websocket_postgres.rb +24 -0
- data/lib/monk/templates/websocket/config/websocket_redis.rb +21 -0
- data/lib/monk/templates/websocket/setup.md +51 -0
- data/lib/monk/templates/websocket/test/websocket_test.rb +21 -0
- data/lib/monk/version.rb +1 -1
- data/lib/monk/websocket/server.rb +3 -1
- metadata +81 -14
- data/lib/monk/scaffold.rb +0 -1063
- data/lib/monk/templates/base/bin/websocket_server +0 -77
- data/lib/monk/templates/jobs/app/jobs/send_login_link.rb +0 -28
- data/lib/monk/templates/live/app/app.rb +0 -22
- data/lib/monk/templates/live/app/views/index.erb +0 -18
- data/lib/monk/templates/live/bin/websocket_server +0 -50
- data/lib/monk/templates/live/config/live_pg.rb +0 -60
- data/lib/monk/templates/mail/Gemfile.extra +0 -1
- data/lib/monk/templates/postgres/Dockerfile +0 -32
- data/lib/monk/templates/postgres/Gemfile.extra +0 -2
- data/lib/monk/templates/redis/Gemfile.extra +0 -1
- /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
|
-
|
|
4
|
-
|
|
5
|
-
require "monk/
|
|
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
|
-
|
|
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
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
require "monk/
|
|
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::
|
|
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
|
-
|
|
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
|
|
@@ -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.
|
|
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
|
-
|
|
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.
|
|
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/
|
|
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
|
|
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/
|
|
252
|
-
- lib/monk/templates/
|
|
253
|
-
- lib/monk/templates/live/
|
|
254
|
-
- lib/monk/templates/live/
|
|
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/
|
|
257
|
-
- lib/monk/templates/
|
|
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/
|
|
260
|
-
- lib/monk/templates/
|
|
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/
|
|
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
|