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
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
ENV["MONK_ENV"] ||= "test"
|
|
2
|
+
|
|
3
|
+
# The test values the modules this app uses wrote (.env.test), loaded
|
|
4
|
+
# before config/settings.rb's .env, which doesn't override them. Only the
|
|
5
|
+
# database name and MONK_ENV differ from development, and they're two
|
|
6
|
+
# separate settings: MONK_ENV=test alone doesn't change DB_NAME.
|
|
7
|
+
env_test = File.expand_path("../.env.test", __dir__)
|
|
8
|
+
if File.exist?(env_test)
|
|
9
|
+
require "dotenv"
|
|
10
|
+
Dotenv.load(env_test)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
require "minitest/autorun"
|
|
14
|
+
require_relative "../config/load"
|
|
15
|
+
require_relative "../app/app"
|
|
16
|
+
|
|
17
|
+
# Booted once, as config.ru does: request tests call APP.
|
|
18
|
+
APP = Monk.boot(App)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## jobs
|
|
2
|
+
|
|
3
|
+
**Where:** `config/jobs.rb` (the queue lives in the `:primary` database),
|
|
4
|
+
job classes in `app/jobs/`, `bin/jobs` runs them.
|
|
5
|
+
|
|
6
|
+
**Main calls:** `class SendInvoice < Monk::Job` with
|
|
7
|
+
`def self.perform(id)`; `SendInvoice.enqueue(id)`, `wait: 300`, `at: time`,
|
|
8
|
+
or `conn: conn` to enqueue inside your own transaction. Class settings:
|
|
9
|
+
`queue "mailers"`, `max_attempts 5`, `never_retry SomeError`. With mail:
|
|
10
|
+
`Monk::Mail.deliver_later(...)`; with auth:
|
|
11
|
+
`Monk::Auth::SendLoginLink.enqueue(email)`.
|
|
12
|
+
|
|
13
|
+
**Test:** `test/jobs_test.rb`. In tests, `Monk::Jobs.drain!` runs every
|
|
14
|
+
enqueued job right away, each in a Ractor; `Monk::Jobs.clear!` in
|
|
15
|
+
`teardown` empties the queue.
|
|
16
|
+
|
|
17
|
+
**Pitfalls:**
|
|
18
|
+
- A job's arguments are stored as JSON: pass ids and plain values, never
|
|
19
|
+
records or objects.
|
|
20
|
+
- A job may run more than once (its process was killed mid-job): make it
|
|
21
|
+
safe to repeat.
|
|
22
|
+
- Jobs run in Ractors: the same shareability rules as routes.
|
|
23
|
+
|
|
24
|
+
**Examples:** `jobs-enqueue` in `app/routes/jobs.rb`; `app/jobs/hello_job.rb`
|
|
25
|
+
is a real, deletable demo job.
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
# Routes that hand work to bin/jobs (monk add jobs).
|
|
2
|
+
#
|
|
3
|
+
# monk:example jobs-enqueue -- a route enqueueing the demo job (app/jobs/hello_job.rb).
|
|
4
|
+
# # bin/jobs runs it, and its line lands in log/development.log:
|
|
5
|
+
# # curl -X POST "http://localhost:9292/jobs/hello?name=Ann"
|
|
6
|
+
# class App
|
|
7
|
+
# post("/jobs/hello") { json(enqueued: HelloJob.enqueue(params[:name] || "world")) }
|
|
8
|
+
# end
|
|
9
|
+
# monk:end
|
|
@@ -1,18 +1,24 @@
|
|
|
1
1
|
#!/usr/bin/env ruby
|
|
2
2
|
require "bundler/setup"
|
|
3
|
-
#
|
|
4
|
-
#
|
|
5
|
-
|
|
6
|
-
require_relative "../config/load"
|
|
7
|
-
require "monk/jobs/runtime"
|
|
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"
|
|
8
6
|
|
|
9
|
-
|
|
10
|
-
#
|
|
11
|
-
|
|
7
|
+
Monk::Bin.run(__FILE__) do
|
|
8
|
+
# The configs and app/, as the web process loads them, so a job can use
|
|
9
|
+
# the app's models and mailers, send mail, or push a Live update --
|
|
10
|
+
# config/load.rb, never Monk.boot: this process serves no routes.
|
|
11
|
+
require_relative "../config/load"
|
|
12
|
+
require "monk/jobs/runtime"
|
|
12
13
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
$stdout.flush
|
|
14
|
+
# The app's templates, so a job can render one (a mail's HTML part, say):
|
|
15
|
+
# there's no App class here to point Monk::Views at them.
|
|
16
|
+
Monk::Views.root = File.expand_path("../app/views", __dir__)
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
workers = Integer(ENV.fetch("JOBS_WORKERS", "5"))
|
|
19
|
+
queues = ENV.fetch("JOBS_QUEUES", "mailers,default").split(",").map(&:strip)
|
|
20
|
+
puts "Monk::Jobs: #{workers} worker(s) on #{queues.join(", ")} -- TERM or Ctrl-C finishes the jobs in hand and stops"
|
|
21
|
+
$stdout.flush
|
|
22
|
+
|
|
23
|
+
Monk::Jobs::Runtime.new(workers: workers, queues: queues).run
|
|
24
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
## jobs
|
|
2
|
+
|
|
3
|
+
Background jobs on the app's own Postgres. A migration creates the queue's
|
|
4
|
+
tables: run `bin/setup_db` and `DB_NAME={{app}}_test bin/setup_db`
|
|
5
|
+
(postgres, above).
|
|
6
|
+
|
|
7
|
+
### Run them
|
|
8
|
+
|
|
9
|
+
`bin/jobs` is the process that runs jobs, next to `bin/server`:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
bin/jobs
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`JOBS_WORKERS` (in `.env`) is how many jobs run at once, and `JOBS_QUEUES`
|
|
16
|
+
which queues it takes them from, in order: `mailers,default` serves mail
|
|
17
|
+
first. `TERM` or Ctrl-C lets the jobs in hand finish before it exits. In
|
|
18
|
+
production it's one more process from the same image, with `bin/jobs` as
|
|
19
|
+
its command.
|
|
20
|
+
|
|
21
|
+
### Check it works
|
|
22
|
+
|
|
23
|
+
1. `bundle exec rake test` runs `test/jobs_test.rb`, which enqueues the
|
|
24
|
+
demo job (`app/jobs/hello_job.rb`) and runs it.
|
|
25
|
+
2. By hand, with `bin/jobs` running: in `bin/console`,
|
|
26
|
+
`HelloJob.enqueue("Ann")`, and `log/development.log` shows
|
|
27
|
+
`Hello, Ann, from a background job`.
|
|
28
|
+
3. The example: uncomment the `jobs-enqueue` block in `app/routes/jobs.rb`,
|
|
29
|
+
restart `bin/server`, and
|
|
30
|
+
`curl -X POST "http://localhost:9292/jobs/hello?name=Bob"`.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
# A job enqueued in the app's database runs: Monk::Jobs.drain! runs every
|
|
4
|
+
# enqueued job right here, each in its own Ractor as bin/jobs would, so a
|
|
5
|
+
# job that only works outside one fails here too. SETUP.md, jobs.
|
|
6
|
+
class JobsTest < Minitest::Test
|
|
7
|
+
def teardown
|
|
8
|
+
Monk::Jobs.clear!
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def test_the_demo_job_runs
|
|
12
|
+
HelloJob.enqueue("test")
|
|
13
|
+
|
|
14
|
+
assert_equal 1, Monk::Jobs.drain!
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## live
|
|
2
|
+
|
|
3
|
+
**Where:** `config/live.rb` (subscribe rules, `live_ws_url`), broadcasts in
|
|
4
|
+
`app/broadcasts/`, the partials they push in `app/views/` (e.g.
|
|
5
|
+
`app/views/live/`), the routes calling them in `app/routes/`.
|
|
6
|
+
|
|
7
|
+
**Main calls:** in a view, `<div <%= live_topic "topic" %>>`; from a route,
|
|
8
|
+
job or broadcast, `Monk::Live.patch(topic, to: "#id", partial: "live/_x",
|
|
9
|
+
**locals)`, also `append`, `prepend`, `remove(topic, to:)`, `batch`. Rules:
|
|
10
|
+
`Monk::Live.authorize("contacts:*") { |subject, topic| ... }` with a block
|
|
11
|
+
built in a module, `anonymous: true` to let visitors in.
|
|
12
|
+
|
|
13
|
+
**Test:** `test/live_test.rb` publishes and checks a subscribed socket gets
|
|
14
|
+
it. To test a page, render it and look for `data-live-topic`.
|
|
15
|
+
|
|
16
|
+
**Pitfalls:**
|
|
17
|
+
- Nothing can be subscribed to until a rule in `config/live.rb` allows it;
|
|
18
|
+
rules are checked in `bin/websocket_server`, which never loads `app/`.
|
|
19
|
+
- A pushed partial sees only its locals (`locals[:text]`), never `params`
|
|
20
|
+
or the session.
|
|
21
|
+
- Each topic name must match what pages subscribe to, exactly.
|
|
22
|
+
- The demo (`/demo/live`) is development only; delete it when done.
|
|
23
|
+
|
|
24
|
+
**Examples:** `live-rule` in `config/live.rb`; `live-broadcast` in
|
|
25
|
+
`app/broadcasts/greeting.rb` and `app/routes/live.rb`.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# What this app pushes to open pages (monk add live).
|
|
2
|
+
#
|
|
3
|
+
# monk:example live-broadcast -- a greeting that changes in every open page at once.
|
|
4
|
+
# # A page shows it in an element subscribed to the topic (and a rule in
|
|
5
|
+
# # config/live.rb lets pages subscribe to "greeting"):
|
|
6
|
+
# # <p <%= live_topic "greeting" %>><%= render "live/_greeting", text: "Hello", layout: false %></p>
|
|
7
|
+
# # The route that changes it is in app/routes/live.rb.
|
|
8
|
+
# module GreetingBroadcast
|
|
9
|
+
# def self.update(text)
|
|
10
|
+
# Monk::Live.patch("greeting", to: "#greeting", partial: "live/_greeting", text: text)
|
|
11
|
+
# end
|
|
12
|
+
# end
|
|
13
|
+
# monk:end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# monk:demo live -- Monk::Live's demo, development only: a counter whose
|
|
2
|
+
# open tabs update together. Open http://localhost:9292/demo/live in two
|
|
3
|
+
# tabs and press the button in one. To remove it, delete this file,
|
|
4
|
+
# app/views/demo/, and the `monk:demo live` lines in config/live.rb.
|
|
5
|
+
if Monk.env.development?
|
|
6
|
+
class App
|
|
7
|
+
# State shared across requests lives in a StateRactor; the update block
|
|
8
|
+
# is built here, where self is shareable, not inline in the route.
|
|
9
|
+
demo_hits = Monk::StateRactor.new(0)
|
|
10
|
+
demo_increment = Ractor.make_shareable(proc { |n| n + 1 })
|
|
11
|
+
|
|
12
|
+
get("/demo/live") { @title = "Monk::Live demo"; render "demo/live", hits: demo_hits.value }
|
|
13
|
+
|
|
14
|
+
# Change state, then tell every open page that shows it. Monk::Live.patch
|
|
15
|
+
# renders the partial once and pushes it to the topic's subscribers.
|
|
16
|
+
post("/demo/live/hit") do
|
|
17
|
+
Monk::Live.patch("demo:hits", to: "#hits", partial: "demo/_hits", hits: demo_hits.update(&demo_increment))
|
|
18
|
+
redirect "/demo/live"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Routes that push updates to open pages (monk add live).
|
|
2
|
+
#
|
|
3
|
+
# monk:example live-broadcast -- the route changing the greeting (app/broadcasts/greeting.rb).
|
|
4
|
+
# class App
|
|
5
|
+
# post("/greeting") do
|
|
6
|
+
# GreetingBroadcast.update(params[:text].to_s)
|
|
7
|
+
# json(updated: true)
|
|
8
|
+
# end
|
|
9
|
+
# end
|
|
10
|
+
# monk:end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<h1>Monk::Live demo</h1>
|
|
2
|
+
<p>
|
|
3
|
+
Open this page in two tabs and press the button in one: the counter in the
|
|
4
|
+
other updates by itself. The server pushes the new HTML over a WebSocket;
|
|
5
|
+
there is no polling and no page JavaScript of your own.
|
|
6
|
+
</p>
|
|
7
|
+
|
|
8
|
+
<%# live_topic makes this element's page subscribe to the "demo:hits" topic. %>
|
|
9
|
+
<p <%= live_topic "demo:hits" %>>Hits so far: <%= render "demo/_hits", hits: locals[:hits], layout: false %></p>
|
|
10
|
+
|
|
11
|
+
<form method="post" action="/demo/live/hit"><button>+1</button></form>
|
|
12
|
+
|
|
13
|
+
<p>
|
|
14
|
+
The pieces: <code>live_topic</code> in this view, the partial
|
|
15
|
+
<code>app/views/demo/_hits.erb</code>, <code>Monk::Live.patch</code> in
|
|
16
|
+
<code>POST /demo/live/hit</code> (<code>app/routes/demo_live.rb</code>), and
|
|
17
|
+
the subscribe rule in <code>config/live.rb</code>.
|
|
18
|
+
</p>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<span id="greeting"><%= locals[:text] %></span>
|
|
@@ -1,47 +1,45 @@
|
|
|
1
|
-
|
|
2
|
-
# publishes updates, e.g. from a route) and bin/websocket_server (the browsers'
|
|
3
|
-
# sockets terminate there and are handed the updates).
|
|
4
|
-
#
|
|
5
|
-
# They are separate processes, so Redis is what carries a publish from one to
|
|
6
|
-
# the other -- REDIS_URL (set in .env by `monk new --live`) is required.
|
|
7
|
-
require_relative "settings"
|
|
1
|
+
require "monk"
|
|
8
2
|
require "monk/live"
|
|
9
|
-
|
|
3
|
+
require_relative "websocket"
|
|
4
|
+
|
|
5
|
+
# Monk::Live (monk add live): HTML pushed to open pages, over the app's
|
|
6
|
+
# WebSocket registry (config/websocket.rb), so it has no transport of its
|
|
7
|
+
# own. Loaded by config/load.rb (bin/server and bin/jobs publish) and by
|
|
8
|
+
# bin/websocket_server (it delivers).
|
|
10
9
|
|
|
11
|
-
# Where the browser opens its WebSocket
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# docs/guides/deploying.md's "Adding Monk::WebSocket" section sets up, so
|
|
16
|
-
# setting PUBLIC_URL alone keeps this in sync instead of two env vars
|
|
17
|
-
# drifting apart. Override with LIVE_WS_URL directly if your setup doesn't
|
|
18
|
-
# fit that shape.
|
|
10
|
+
# Where the browser opens its WebSocket, for the layout's <%= monk_head %>:
|
|
11
|
+
# the direct port in development; elsewhere a /ws path under public_url
|
|
12
|
+
# (config/settings.rb), matching the proxy docs/guides/deploying.md sets
|
|
13
|
+
# up, so PUBLIC_URL alone keeps both in sync. LIVE_WS_URL overrides it.
|
|
19
14
|
default_live_ws_url =
|
|
20
15
|
if Monk.env.development?
|
|
21
16
|
"ws://localhost:9293"
|
|
22
17
|
else
|
|
23
|
-
Monk::Settings[:public_url].sub(
|
|
18
|
+
Monk::Settings[:public_url].sub(%r{\Ahttps://}, "wss://").sub(%r{\Ahttp://}, "ws://") + "/ws"
|
|
24
19
|
end
|
|
25
20
|
|
|
26
21
|
Monk::Settings.configure do
|
|
27
22
|
optional :live_ws_url, default: default_live_ws_url
|
|
28
23
|
end
|
|
29
24
|
|
|
30
|
-
|
|
31
|
-
"are separate processes, and Redis is how an update published in one reaches a socket in the other"
|
|
32
|
-
|
|
33
|
-
Monk::Live.configure(
|
|
34
|
-
registry: Monk::WebSocket::RedisFanout.new(Monk::WebSocket::Registry.new, redis_url: redis_url),
|
|
35
|
-
)
|
|
25
|
+
Monk::Live.configure(registry: AppWebSocket::REGISTRY)
|
|
36
26
|
|
|
37
27
|
# Who may subscribe to what. Nothing is allowed unless a rule says so, and an
|
|
38
|
-
# anonymous
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
28
|
+
# anonymous socket is denied unless its rule says `anonymous: true`. Rule
|
|
29
|
+
# blocks must be Ractor-shareable, so they're built in a module (self at the
|
|
30
|
+
# top of this file is not); Monk::Live::ALLOW_ALL lets everyone in.
|
|
31
|
+
#
|
|
32
|
+
# monk:example live-rule -- each person may subscribe to their own contacts topic.
|
|
33
|
+
# # subject is the logged-in user (with auth), nil for a visitor.
|
|
34
|
+
# module AppLive
|
|
35
|
+
# OWN_CONTACTS = proc { |subject, topic| topic == "contacts:#{subject}" }
|
|
36
|
+
# end
|
|
37
|
+
# Monk::Live.authorize("contacts:*", &AppLive::OWN_CONTACTS)
|
|
38
|
+
# monk:end
|
|
46
39
|
|
|
47
|
-
|
|
40
|
+
# monk:demo live -- the demo's topic (app/routes/demo_live.rb), development
|
|
41
|
+
# only. Remove these lines with it.
|
|
42
|
+
if Monk.env.development?
|
|
43
|
+
Monk::Live.authorize("demo:hits", anonymous: true, &Monk::Live::ALLOW_ALL)
|
|
44
|
+
end
|
|
45
|
+
# monk:end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
require "monk"
|
|
2
|
+
require "monk/live"
|
|
3
|
+
require_relative "websocket"
|
|
4
|
+
|
|
5
|
+
# Monk::Live (monk add live): HTML pushed to open pages, over the app's
|
|
6
|
+
# WebSocket registry (config/websocket.rb), so it has no transport of its
|
|
7
|
+
# own. Loaded by config/load.rb (bin/server and bin/jobs publish) and by
|
|
8
|
+
# bin/websocket_server (it delivers).
|
|
9
|
+
|
|
10
|
+
# Where the browser opens its WebSocket, for the layout's <%= monk_head %>:
|
|
11
|
+
# the direct port in development; elsewhere a /ws path under public_url
|
|
12
|
+
# (config/settings.rb), matching the proxy docs/guides/deploying.md sets
|
|
13
|
+
# up, so PUBLIC_URL alone keeps both in sync. LIVE_WS_URL overrides it.
|
|
14
|
+
default_live_ws_url =
|
|
15
|
+
if Monk.env.development?
|
|
16
|
+
"ws://localhost:9293"
|
|
17
|
+
else
|
|
18
|
+
Monk::Settings[:public_url].sub(%r{\Ahttps://}, "wss://").sub(%r{\Ahttp://}, "ws://") + "/ws"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
Monk::Settings.configure do
|
|
22
|
+
optional :live_ws_url, default: default_live_ws_url
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
Monk::Live.configure(registry: AppWebSocket::REGISTRY)
|
|
26
|
+
|
|
27
|
+
# Who may subscribe to what. Nothing is allowed unless a rule says so, and an
|
|
28
|
+
# anonymous socket is denied unless its rule says `anonymous: true`. Rule
|
|
29
|
+
# blocks must be Ractor-shareable, so they're built in a module (self at the
|
|
30
|
+
# top of this file is not); Monk::Live::ALLOW_ALL lets everyone in.
|
|
31
|
+
#
|
|
32
|
+
# monk:example live-rule -- each person may subscribe to their own contacts topic.
|
|
33
|
+
# # subject is the logged-in user (with auth), nil for a visitor.
|
|
34
|
+
# module AppLive
|
|
35
|
+
# OWN_CONTACTS = proc { |subject, topic| topic == "contacts:#{subject}" }
|
|
36
|
+
# end
|
|
37
|
+
# Monk::Live.authorize("contacts:*", &AppLive::OWN_CONTACTS)
|
|
38
|
+
# monk:end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
## live
|
|
2
|
+
|
|
3
|
+
HTML pushed to open pages over the WebSocket (websocket, above): a route
|
|
4
|
+
changes something and calls `Monk::Live.patch`; every page showing it
|
|
5
|
+
updates, with no page JavaScript of your own. The layout's
|
|
6
|
+
`<%= monk_head %>` loads the browser client, which Monk serves from the gem.
|
|
7
|
+
|
|
8
|
+
### Try the demo
|
|
9
|
+
|
|
10
|
+
With `bin/server` and `bin/websocket_server` running, open
|
|
11
|
+
http://localhost:9292/demo/live in two tabs and press the button in one:
|
|
12
|
+
the other updates by itself. The demo exists in development only. To
|
|
13
|
+
remove it, delete `app/routes/demo_live.rb`, `app/views/demo/`, and the
|
|
14
|
+
`monk:demo live` lines in `config/live.rb`.
|
|
15
|
+
|
|
16
|
+
### Before production
|
|
17
|
+
|
|
18
|
+
`LIVE_WS_URL` defaults to a `/ws` path under `PUBLIC_URL`, with `wss://`
|
|
19
|
+
for an `https://` origin: the proxy in front routes `/ws` to
|
|
20
|
+
`bin/websocket_server` (docs/guides/deploying.md in the monk repo).
|
|
21
|
+
|
|
22
|
+
### Check it works
|
|
23
|
+
|
|
24
|
+
1. `bundle exec rake test` runs `test/live_test.rb`: an update reaches a
|
|
25
|
+
socket subscribed to its topic.
|
|
26
|
+
2. By hand: the demo, above.
|
|
27
|
+
3. The example: uncomment `live-broadcast` (in
|
|
28
|
+
`app/broadcasts/greeting.rb` and `app/routes/live.rb`) and add a rule
|
|
29
|
+
for its topic in `config/live.rb`, e.g.
|
|
30
|
+
`Monk::Live.authorize("greeting", anonymous: true, &Monk::Live::ALLOW_ALL)`.
|
|
31
|
+
Put `<p <%= live_topic "greeting" %>><%= render "live/_greeting", text: "Hello", layout: false %></p>`
|
|
32
|
+
in a page, open it in two tabs, and change the greeting:
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
curl -X POST http://localhost:9292/greeting -H "content-type: application/json" \
|
|
36
|
+
-d '{"text": "Hi all"}'
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Both tabs update.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
require "json"
|
|
3
|
+
require "timeout"
|
|
4
|
+
|
|
5
|
+
# Monk::Live publishes through the app's WebSocket registry
|
|
6
|
+
# (config/websocket.rb): an update reaches a socket subscribed to its topic.
|
|
7
|
+
# SETUP.md, live.
|
|
8
|
+
class LiveTest < Minitest::Test
|
|
9
|
+
def test_an_update_reaches_a_subscribed_socket
|
|
10
|
+
assert_same AppWebSocket::REGISTRY, Monk::Live.registry
|
|
11
|
+
AppWebSocket::REGISTRY.listen!
|
|
12
|
+
port = Ractor::Port.new
|
|
13
|
+
AppWebSocket::REGISTRY.register(:"live:check", port)
|
|
14
|
+
|
|
15
|
+
Monk::Live.remove("live:check", to: "#gone")
|
|
16
|
+
|
|
17
|
+
assert_equal "remove", JSON.parse(Timeout.timeout(5) { port.receive })["mode"]
|
|
18
|
+
ensure
|
|
19
|
+
AppWebSocket::REGISTRY.unregister(:"live:check", port) if port
|
|
20
|
+
port&.close
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
## mail
|
|
2
|
+
|
|
3
|
+
**Where:** `config/mail.rb` (from `MAIL_URL`, `MAIL_FROM`), mailers in
|
|
4
|
+
`app/mailers/` (module methods), HTML parts in `app/views/mail/`.
|
|
5
|
+
|
|
6
|
+
**Main calls:** `Monk::Mail.deliver(to:, subject:, text:, html:)`;
|
|
7
|
+
`Monk::Mail.render("mail/<template>", **locals)` for the HTML part (the
|
|
8
|
+
template reads `locals[:name]`). With jobs:
|
|
9
|
+
`Monk::Mail.deliver_later(...)`, the same arguments.
|
|
10
|
+
|
|
11
|
+
**Test:** `test/mail_test.rb` delivers to `log/test.log` (`MAIL_URL=log://`
|
|
12
|
+
in `.env.test`); assert on that file's lines.
|
|
13
|
+
|
|
14
|
+
**Pitfalls:**
|
|
15
|
+
- `deliver` waits for the mail server. In a request that matters: use
|
|
16
|
+
`deliver_later` once jobs are installed.
|
|
17
|
+
- Render the HTML before `deliver_later`: the job only sends.
|
|
18
|
+
- `MAIL_URL` is required outside development; `MAIL_FROM` must be on a
|
|
19
|
+
verified domain, or mail lands in spam.
|
|
20
|
+
|
|
21
|
+
**Examples:** `mail-welcome` in `app/mailers/app_mailer.rb` and
|
|
22
|
+
`app/routes/mail.rb`.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
# The emails this app sends, through Monk::Mail (config/mail.rb). Module
|
|
2
|
+
# methods, so a route or a job can call them from any Ractor.
|
|
3
|
+
module AppMailer
|
|
4
|
+
# monk:example mail-welcome -- an email, sent from a route (app/routes/mail.rb).
|
|
5
|
+
# def self.welcome(to:, name:)
|
|
6
|
+
# Monk::Mail.deliver(
|
|
7
|
+
# to: to,
|
|
8
|
+
# subject: "Welcome, #{name}",
|
|
9
|
+
# text: "Hello #{name}, thanks for signing up.",
|
|
10
|
+
# html: Monk::Mail.render("mail/welcome", name: name),
|
|
11
|
+
# )
|
|
12
|
+
# end
|
|
13
|
+
# monk:end
|
|
14
|
+
end
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# Routes that send email (monk add mail).
|
|
2
|
+
#
|
|
3
|
+
# monk:example mail-welcome -- the route that sends AppMailer.welcome
|
|
4
|
+
# # (app/mailers/app_mailer.rb). Sending waits for the mail server; with
|
|
5
|
+
# # monk add jobs, send it from a job instead (AGENTS.md, mail).
|
|
6
|
+
# class App
|
|
7
|
+
# post("/welcome") do
|
|
8
|
+
# AppMailer.welcome(to: params[:email].to_s, name: params[:name].to_s)
|
|
9
|
+
# json(sent: true)
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
# monk:end
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<p>Hello <%= locals[:name] %>, thanks for signing up.</p>
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
require "monk"
|
|
2
2
|
require "monk/mail"
|
|
3
3
|
|
|
4
|
-
# Sends email with Monk::Mail.deliver(to:, subject:, text:, html:)
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# config/auth.rb: bin/websocket_server loads that
|
|
4
|
+
# Sends email with Monk::Mail.deliver(to:, subject:, text:, html:): the
|
|
5
|
+
# app's mailers (app/mailers/) and, with auth, its magic links. See
|
|
6
|
+
# docs/guides/mail.md in the monk repo. Loaded by config/load.rb, never by
|
|
7
|
+
# config/auth.rb: bin/websocket_server loads that, and never sends mail.
|
|
8
8
|
Monk::Settings.configure do
|
|
9
9
|
# Where mail goes. Unset in development means log:// (printed to the
|
|
10
10
|
# console, nothing sent); anywhere else an unset one fails the boot.
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
## mail
|
|
2
|
+
|
|
3
|
+
In development `MAIL_URL` is unset, so every message is printed to the
|
|
4
|
+
console (and `log/development.log`) instead of sent. Tests use
|
|
5
|
+
`MAIL_URL=log://` (`.env.test`): messages go to `log/test.log`.
|
|
6
|
+
|
|
7
|
+
### Before production
|
|
8
|
+
|
|
9
|
+
Set `MAIL_URL` to your provider's SMTP relay, e.g.
|
|
10
|
+
`smtp://user:password@smtp.provider.com:587`, and `MAIL_FROM` to a sender on
|
|
11
|
+
a domain you've verified with that provider (SPF, DKIM). An unset `MAIL_URL`
|
|
12
|
+
fails the boot outside development. Provider examples:
|
|
13
|
+
`docs/guides/mail.md` in the monk repo.
|
|
14
|
+
|
|
15
|
+
### Check it works
|
|
16
|
+
|
|
17
|
+
1. `bundle exec rake test` runs `test/mail_test.rb`, which delivers a
|
|
18
|
+
message to `log/test.log`.
|
|
19
|
+
2. By hand, in `bin/console`:
|
|
20
|
+
`Monk::Mail.deliver(to: "you@example.com", subject: "Hi", text: "Hello")`.
|
|
21
|
+
In development the message is printed right there (and in
|
|
22
|
+
`log/development.log`), not sent.
|
|
23
|
+
3. The example: uncomment the `mail-welcome` blocks in
|
|
24
|
+
`app/mailers/app_mailer.rb` and `app/routes/mail.rb`, restart
|
|
25
|
+
`bin/server`, and send one:
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
curl -X POST http://localhost:9292/welcome -H "content-type: application/json" \
|
|
29
|
+
-d '{"email": "ann@example.com", "name": "Ann"}'
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
The welcome email is printed in `bin/server`'s console.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
require_relative "test_helper"
|
|
2
|
+
|
|
3
|
+
# Monk::Mail is configured, and with .env.test's MAIL_URL=log:// a message
|
|
4
|
+
# lands in log/test.log instead of being sent: SETUP.md, mail.
|
|
5
|
+
class MailTest < Minitest::Test
|
|
6
|
+
def test_a_message_is_delivered_to_the_test_log
|
|
7
|
+
subject = "Mail check #{rand(1_000_000)}"
|
|
8
|
+
|
|
9
|
+
Monk::Mail.deliver(to: "ann@example.test", subject: subject, text: "Hello")
|
|
10
|
+
|
|
11
|
+
assert_includes File.read(File.expand_path("../log/test.log", __dir__)), subject.inspect
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
## postgres
|
|
2
|
+
|
|
3
|
+
**Where:** `config/persistence.rb` registers the `:primary` connection from
|
|
4
|
+
`DB_*`. Migrations: `db/migrate/<timestamp>_<name>.up.sql` and `.down.sql`,
|
|
5
|
+
plain SQL. Models: `app/models/`.
|
|
6
|
+
|
|
7
|
+
**Main calls:** a model is `class Note < Monk::Persistence::Pg::Model` with
|
|
8
|
+
`self.db_name = :primary` and `self.table_name = "notes"`; then
|
|
9
|
+
`Note.create(...)`, `.find(id)`, `.where(...)`, `.update(id, ...)`,
|
|
10
|
+
`.delete(id)`. Rows are plain Symbol-keyed Hashes: no ORM, no associations.
|
|
11
|
+
Raw SQL: `Monk::Persistence::Pg.checkout(:primary) { |conn| conn.exec_params(sql, params) }`.
|
|
12
|
+
Commands: `bin/setup_db` (apply pending migrations), `bin/migrate
|
|
13
|
+
[migrate|rollback N|status]`, `bin/console`.
|
|
14
|
+
|
|
15
|
+
**Test:** `test/persistence_test.rb` connects to the test database. Tests use
|
|
16
|
+
`.env.test`'s `DB_NAME`: migrate it with `DB_NAME={{app}}_test bin/setup_db`.
|
|
17
|
+
|
|
18
|
+
**Pitfalls:**
|
|
19
|
+
- Models and connections must exist before `Monk.boot`: put models in
|
|
20
|
+
`app/models/`, never define one inside a route.
|
|
21
|
+
- Never interpolate params into SQL: use `exec_params` with `$1`, `$2`.
|
|
22
|
+
- A new migration gets a new, later timestamp. Never edit one that's
|
|
23
|
+
already applied.
|
|
24
|
+
|
|
25
|
+
**Examples:** `postgres-model` in `app/routes/postgres.rb`.
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# Routes that read and write the app's own tables (monk add postgres).
|
|
2
|
+
#
|
|
3
|
+
# monk:example postgres-model -- a table, a model, and the routes using it.
|
|
4
|
+
# # Uncomment, then move each part where its comment says.
|
|
5
|
+
#
|
|
6
|
+
# # 1. The table, in a migration: db/migrate/<timestamp>_create_notes.up.sql
|
|
7
|
+
# # (and .down.sql with DROP TABLE notes;), then bin/setup_db.
|
|
8
|
+
# #
|
|
9
|
+
# # CREATE TABLE notes (id BIGSERIAL PRIMARY KEY, body TEXT NOT NULL);
|
|
10
|
+
#
|
|
11
|
+
# # 2. The model, in app/models/note.rb. Rows are plain Hashes, not objects.
|
|
12
|
+
# class Note < Monk::Persistence::Pg::Model
|
|
13
|
+
# self.db_name = :primary
|
|
14
|
+
# self.table_name = "notes"
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# # 3. The routes, here. params come from the query string or a JSON body.
|
|
18
|
+
# class App
|
|
19
|
+
# get("/notes") { json(notes: Note.where({}, order: :id, limit: 20)) }
|
|
20
|
+
# post("/notes") { json(note: Note.create(body: params[:body].to_s)) }
|
|
21
|
+
# end
|
|
22
|
+
# monk:end
|
|
@@ -3,7 +3,9 @@ require "bundler/setup"
|
|
|
3
3
|
# The configs and app/ (models, jobs, mailers...), as the app sees them --
|
|
4
4
|
# config/load.rb, never Monk.boot, so nothing is frozen and you can poke at
|
|
5
5
|
# anything.
|
|
6
|
-
|
|
6
|
+
# A service that's down or a missing setting ends here with what to fix.
|
|
7
|
+
require "monk/bin"
|
|
8
|
+
Monk::Bin.run(__FILE__) { require_relative "../config/load" }
|
|
7
9
|
require "irb"
|
|
8
10
|
|
|
9
11
|
IRB.start(__FILE__)
|