monkrb 0.18.0 → 0.20.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 +166 -0
- data/README.md +2 -2
- data/lib/monk/base.rb +20 -0
- data/lib/monk/jobs/adapters/pg.rb +3 -1
- data/lib/monk/live/client/monk_live.js +2 -2
- data/lib/monk/live/client/protocol.js +5 -0
- data/lib/monk/live/policy.rb +39 -4
- data/lib/monk/live/session.rb +7 -0
- data/lib/monk/live.rb +15 -3
- data/lib/monk/persistence/errors.rb +32 -0
- data/lib/monk/persistence/pg.rb +92 -1
- data/lib/monk/persistence/pool.rb +513 -0
- data/lib/monk/persistence.rb +196 -7
- data/lib/monk/scaffold.rb +155 -78
- data/lib/monk/templates/auth/app/mailers/app_mailer.rb +22 -0
- data/lib/monk/templates/auth/config/auth.rb +13 -20
- data/lib/monk/templates/base/app/app.rb +9 -0
- data/lib/monk/templates/base/app/broadcasts/.keep +0 -0
- data/lib/monk/templates/base/app/helpers/.keep +0 -0
- data/lib/monk/templates/base/app/jobs/.keep +0 -0
- data/lib/monk/templates/base/app/mailers/.keep +0 -0
- data/lib/monk/templates/base/app/models/.keep +0 -0
- data/lib/monk/templates/base/app/presenters/.keep +0 -0
- data/lib/monk/templates/base/bin/websocket_server +17 -2
- data/lib/monk/templates/base/config/load.rb +18 -0
- data/lib/monk/templates/base/config.ru +3 -12
- data/lib/monk/templates/base/public/js/app.js +1 -1
- data/lib/monk/templates/jobs/{jobs → app/jobs}/send_login_link.rb +2 -1
- data/lib/monk/templates/jobs/bin/jobs +5 -13
- data/lib/monk/templates/jobs/config/jobs.rb +2 -4
- data/lib/monk/templates/live/{config.ru → app/app.rb} +1 -6
- data/lib/monk/templates/live/{views → app/views}/index.erb +2 -2
- data/lib/monk/templates/live/bin/websocket_server +23 -3
- data/lib/monk/templates/live/config/live.rb +1 -1
- data/lib/monk/templates/live/config/live_pg.rb +1 -1
- data/lib/monk/templates/mail/config/mail.rb +2 -2
- data/lib/monk/templates/postgres/bin/console +4 -2
- data/lib/monk/templates/postgres/db/migrate/.keep +0 -0
- data/lib/monk/version.rb +1 -1
- data/lib/monk/websocket/connection.rb +25 -6
- data/lib/monk/websocket/errors.rb +11 -0
- data/lib/monk/websocket/listeners.rb +47 -0
- data/lib/monk/websocket/pg_fanout.rb +189 -31
- data/lib/monk/websocket/redis_fanout.rb +125 -33
- data/lib/monk/websocket/registry.rb +30 -0
- data/lib/monk/websocket/server.rb +128 -29
- data/lib/monk/websocket.rb +2 -0
- data/lib/monk.rb +10 -0
- metadata +21 -9
- /data/lib/monk/templates/auth/{views → app/views}/mail/magic_link.erb +0 -0
- /data/lib/monk/templates/base/{views → app/views}/index.erb +0 -0
- /data/lib/monk/templates/base/{views → app/views}/layouts/app.erb +0 -0
- /data/lib/monk/templates/jobs/{jobs → app/jobs}/hello_job.rb +0 -0
- /data/lib/monk/templates/live/{views → app/views}/live/_hits.erb +0 -0
data/lib/monk/scaffold.rb
CHANGED
|
@@ -10,18 +10,26 @@ module Monk
|
|
|
10
10
|
class Scaffold
|
|
11
11
|
TEMPLATES_DIR = File.expand_path("templates", __dir__)
|
|
12
12
|
|
|
13
|
+
# The role directories under app/, in config/load.rb's load order.
|
|
14
|
+
APP_ROLES = %w[models presenters helpers mailers broadcasts jobs].freeze
|
|
15
|
+
|
|
13
16
|
BASE_FILES = {
|
|
14
17
|
"Gemfile" => "base/Gemfile",
|
|
15
18
|
"config.ru" => "base/config.ru",
|
|
16
19
|
"config/settings.rb" => "base/config/settings.rb",
|
|
20
|
+
"config/load.rb" => "base/config/load.rb",
|
|
21
|
+
"app/app.rb" => "base/app/app.rb",
|
|
17
22
|
".ruby-version" => "base/.ruby-version",
|
|
18
23
|
".gitignore" => "base/.gitignore",
|
|
19
24
|
".dockerignore" => "base/.dockerignore",
|
|
20
25
|
"Dockerfile" => "base/Dockerfile",
|
|
21
26
|
"bin/server" => "base/bin/server",
|
|
22
27
|
"bin/websocket_server" => "base/bin/websocket_server",
|
|
23
|
-
"views/layouts/app.erb" => "base/views/layouts/app.erb",
|
|
24
|
-
"views/index.erb" => "base/views/index.erb",
|
|
28
|
+
"app/views/layouts/app.erb" => "base/app/views/layouts/app.erb",
|
|
29
|
+
"app/views/index.erb" => "base/app/views/index.erb",
|
|
30
|
+
# Every role directory, whatever the flags (docs/adr/0015): the tree
|
|
31
|
+
# shows where each kind of code goes. Flags add files next to these.
|
|
32
|
+
**APP_ROLES.to_h { |role| ["app/#{role}/.keep", "base/app/#{role}/.keep"] },
|
|
25
33
|
"public/css/app.css" => "base/public/css/app.css",
|
|
26
34
|
"public/js/app.js" => "base/public/js/app.js",
|
|
27
35
|
}.freeze
|
|
@@ -31,6 +39,9 @@ module Monk
|
|
|
31
39
|
"bin/console" => "postgres/bin/console",
|
|
32
40
|
"bin/setup_db" => "postgres/bin/setup_db",
|
|
33
41
|
"bin/migrate" => "postgres/bin/migrate",
|
|
42
|
+
# Kept by git when empty, like app/'s role directories: a fresh clone
|
|
43
|
+
# still shows where migrations go. The migrator reads only *.sql.
|
|
44
|
+
"db/migrate/.keep" => "postgres/db/migrate/.keep",
|
|
34
45
|
}.freeze
|
|
35
46
|
|
|
36
47
|
# The pg gem's native extension needs libpq -- the base Dockerfile has
|
|
@@ -45,9 +56,11 @@ module Monk
|
|
|
45
56
|
"config/auth.rb" => "auth/config/auth.rb",
|
|
46
57
|
"db/migrate/00000000000001_create_auth_tables.up.sql" => "auth/db/migrate/00000000000001_create_auth_tables.up.sql",
|
|
47
58
|
"db/migrate/00000000000001_create_auth_tables.down.sql" => "auth/db/migrate/00000000000001_create_auth_tables.down.sql",
|
|
48
|
-
# The magic link's
|
|
49
|
-
#
|
|
50
|
-
|
|
59
|
+
# The magic link's sender, Monk::Auth's deliver: hook -- config/auth.rb
|
|
60
|
+
# requires it -- and the HTML part it renders and sends through
|
|
61
|
+
# Monk::Mail (MAIL_FILES, implied by --auth).
|
|
62
|
+
"app/mailers/app_mailer.rb" => "auth/app/mailers/app_mailer.rb",
|
|
63
|
+
"app/views/mail/magic_link.erb" => "auth/app/views/mail/magic_link.erb",
|
|
51
64
|
}.freeze
|
|
52
65
|
|
|
53
66
|
# --mail, or implied by --auth: Monk::Mail's config. Its Gemfile line
|
|
@@ -61,7 +74,7 @@ module Monk
|
|
|
61
74
|
# numbered after auth's so the two sort in order when both are present.
|
|
62
75
|
JOBS_FILES = {
|
|
63
76
|
"config/jobs.rb" => "jobs/config/jobs.rb",
|
|
64
|
-
"jobs/hello_job.rb" => "jobs/jobs/hello_job.rb",
|
|
77
|
+
"app/jobs/hello_job.rb" => "jobs/app/jobs/hello_job.rb",
|
|
65
78
|
"bin/jobs" => "jobs/bin/jobs",
|
|
66
79
|
"db/migrate/00000000000002_create_jobs_tables.up.sql" => "jobs/db/migrate/00000000000002_create_jobs_tables.up.sql",
|
|
67
80
|
"db/migrate/00000000000002_create_jobs_tables.down.sql" =>
|
|
@@ -71,7 +84,7 @@ module Monk
|
|
|
71
84
|
# --auth --jobs: the magic link is created and sent inside a job, so the
|
|
72
85
|
# raw token is never stored (docs/adr/0014).
|
|
73
86
|
JOBS_AUTH_FILES = {
|
|
74
|
-
"jobs/send_login_link.rb" => "jobs/jobs/send_login_link.rb",
|
|
87
|
+
"app/jobs/send_login_link.rb" => "jobs/app/jobs/send_login_link.rb",
|
|
75
88
|
}.freeze
|
|
76
89
|
|
|
77
90
|
# --mail --jobs (or --auth --jobs): Monk::Mail.deliver_later, loaded in
|
|
@@ -79,34 +92,34 @@ module Monk
|
|
|
79
92
|
JOBS_REQUIRE_ANCHOR = %(require "monk/jobs"\n).freeze
|
|
80
93
|
JOBS_MAIL_REQUIRE = %(require "monk/mail/later"\n).freeze
|
|
81
94
|
|
|
82
|
-
# --jobs's demo route, added right after this line in
|
|
95
|
+
# --jobs's demo route, added right after this line in app/app.rb (the
|
|
83
96
|
# base and the --live one both end their routes with it), so the
|
|
84
97
|
# round trip -- enqueue from a request, run in bin/jobs -- works out of
|
|
85
98
|
# the box.
|
|
86
99
|
JOBS_ROUTE_ANCHOR = %( get("/api/hello") { json(message: "hello from monk") }\n).freeze
|
|
87
100
|
JOBS_ROUTE = <<~RUBY.gsub(/^(?!$)/, " ").freeze
|
|
88
101
|
|
|
89
|
-
# Enqueues the demo job (jobs/hello_job.rb) for bin/jobs to run:
|
|
102
|
+
# Enqueues the demo job (app/jobs/hello_job.rb) for bin/jobs to run:
|
|
90
103
|
# curl -X POST "http://localhost:9292/jobs/hello?name=Ann"
|
|
91
104
|
post("/jobs/hello") { json(enqueued: HelloJob.enqueue(params[:name] || "world")) }
|
|
92
105
|
RUBY
|
|
93
106
|
|
|
94
107
|
# --live: Monk::Live's demo (a counter whose open tabs update when
|
|
95
108
|
# another request changes it). These replace three base files outright
|
|
96
|
-
# rather than patching them -- a live
|
|
97
|
-
# files, not one line of diff -- and add two. config.ru
|
|
98
|
-
#
|
|
109
|
+
# rather than patching them -- a live app and index are different
|
|
110
|
+
# files, not one line of diff -- and add two. config.ru stays the base
|
|
111
|
+
# one: config/load.rb is where --live's config gets wired in.
|
|
99
112
|
LIVE_OVERRIDES = {
|
|
100
|
-
"
|
|
113
|
+
"app/app.rb" => "live/app/app.rb",
|
|
101
114
|
"bin/websocket_server" => "live/bin/websocket_server",
|
|
102
|
-
"views/index.erb" => "live/views/index.erb",
|
|
115
|
+
"app/views/index.erb" => "live/app/views/index.erb",
|
|
103
116
|
}.freeze
|
|
104
117
|
|
|
105
118
|
# config/live.rb itself isn't here -- #write_live! picks between
|
|
106
119
|
# LIVE_CONFIG_TEMPLATES[@live_transport] instead, since which one gets
|
|
107
120
|
# written depends on --redis vs --postgres, not a fixed mapping.
|
|
108
121
|
LIVE_FILES = {
|
|
109
|
-
"views/live/_hits.erb" => "live/views/live/_hits.erb",
|
|
122
|
+
"app/views/live/_hits.erb" => "live/app/views/live/_hits.erb",
|
|
110
123
|
}.freeze
|
|
111
124
|
|
|
112
125
|
LIVE_CONFIG_TEMPLATES = {
|
|
@@ -219,7 +232,6 @@ module Monk
|
|
|
219
232
|
|
|
220
233
|
if @postgres
|
|
221
234
|
POSTGRES_FILES.each { |relative, template| write_file(relative, template, executable: EXECUTABLE_FILES.include?(relative)) }
|
|
222
|
-
FileUtils.mkdir_p(File.join(@dir, "db/migrate"))
|
|
223
235
|
append_gemfile_extra("postgres/Gemfile.extra")
|
|
224
236
|
end
|
|
225
237
|
|
|
@@ -235,7 +247,7 @@ module Monk
|
|
|
235
247
|
add_deliver_later! if @mail
|
|
236
248
|
end
|
|
237
249
|
|
|
238
|
-
|
|
250
|
+
wire_load! if @postgres || @mail || @live
|
|
239
251
|
append_gemfile_extra("redis/Gemfile.extra") if @redis
|
|
240
252
|
|
|
241
253
|
# --postgres, --redis and --mail are the flags with anything worth
|
|
@@ -272,7 +284,7 @@ module Monk
|
|
|
272
284
|
|
|
273
285
|
def combinations
|
|
274
286
|
pairs = []
|
|
275
|
-
pairs << ["--auth + --jobs", "jobs/send_login_link.rb: login links are sent from a job"] if @auth && @jobs
|
|
287
|
+
pairs << ["--auth + --jobs", "app/jobs/send_login_link.rb: login links are sent from a job"] if @auth && @jobs
|
|
276
288
|
if @mail && @jobs
|
|
277
289
|
pairs << ["--mail + --jobs",
|
|
278
290
|
"config/jobs.rb loads Monk::Mail.deliver_later; JOBS_QUEUES serves mailers first",]
|
|
@@ -307,7 +319,7 @@ module Monk
|
|
|
307
319
|
end
|
|
308
320
|
|
|
309
321
|
def add_live_to_layout!
|
|
310
|
-
path = File.join(@dir, "views/layouts/app.erb")
|
|
322
|
+
path = File.join(@dir, "app/views/layouts/app.erb")
|
|
311
323
|
content = File.read(path)
|
|
312
324
|
raise "layout wiring failed: </head> not found" unless content.include?(" </head>\n")
|
|
313
325
|
|
|
@@ -337,36 +349,37 @@ module Monk
|
|
|
337
349
|
File.write(path, content.sub(DOTENV_COMMENTED_LINE, DOTENV_LINE))
|
|
338
350
|
end
|
|
339
351
|
|
|
340
|
-
# config.
|
|
341
|
-
#
|
|
342
|
-
#
|
|
352
|
+
# config/load.rb ships in BASE_FILES unconditionally (config.ru and the
|
|
353
|
+
# tests require it), so the flags can't gate whether it exists, only
|
|
354
|
+
# which module configs it requires. Unlike BASE_FILES/POSTGRES_FILES/
|
|
343
355
|
# AUTH_FILES, this is a post-write edit rather than a verbatim copy --
|
|
344
|
-
# the alternative (a second, fuller
|
|
345
|
-
# combination) would duplicate the whole file for
|
|
346
|
-
def
|
|
347
|
-
path = File.join(@dir, "config.
|
|
348
|
-
settings_require = %(require_relative "
|
|
356
|
+
# the alternative (a second, fuller load.rb template per flag
|
|
357
|
+
# combination) would duplicate the whole file for a few lines of diff.
|
|
358
|
+
def wire_load!
|
|
359
|
+
path = File.join(@dir, "config/load.rb")
|
|
360
|
+
settings_require = %(require_relative "settings"\n)
|
|
349
361
|
requires = +""
|
|
350
362
|
if @postgres
|
|
351
|
-
target = @auth ? "
|
|
363
|
+
target = @auth ? "auth" : "persistence" # config/auth.rb itself require_relative "persistence"
|
|
352
364
|
requires << "require_relative \"#{target}\"\n"
|
|
353
365
|
end
|
|
354
|
-
# config/mail.rb from config.
|
|
366
|
+
# config/mail.rb from config/load.rb only, never from config/auth.rb:
|
|
355
367
|
# bin/websocket_server loads config/auth.rb too and never sends mail,
|
|
356
368
|
# so it shouldn't need MAIL_URL to boot.
|
|
357
|
-
requires << "require_relative \"
|
|
358
|
-
requires << "require_relative \"
|
|
369
|
+
requires << "require_relative \"mail\"\n" if @mail
|
|
370
|
+
requires << "require_relative \"jobs\"\n" if @jobs
|
|
371
|
+
requires << "require_relative \"live\"\n" if @live
|
|
359
372
|
|
|
360
373
|
content = File.read(path)
|
|
361
|
-
raise "config.
|
|
374
|
+
raise "config/load.rb wiring failed: #{settings_require.inspect} not found" unless content.include?(settings_require)
|
|
362
375
|
|
|
363
376
|
File.write(path, content.sub(settings_require, "#{settings_require}#{requires}"))
|
|
364
377
|
end
|
|
365
378
|
|
|
366
379
|
def add_jobs_route!
|
|
367
|
-
path = File.join(@dir, "
|
|
380
|
+
path = File.join(@dir, "app/app.rb")
|
|
368
381
|
content = File.read(path)
|
|
369
|
-
raise "
|
|
382
|
+
raise "app/app.rb wiring failed: #{JOBS_ROUTE_ANCHOR.inspect} not found" unless content.include?(JOBS_ROUTE_ANCHOR)
|
|
370
383
|
|
|
371
384
|
File.write(path, content.sub(JOBS_ROUTE_ANCHOR, "#{JOBS_ROUTE_ANCHOR}#{JOBS_ROUTE}"))
|
|
372
385
|
end
|
|
@@ -419,12 +432,16 @@ module Monk
|
|
|
419
432
|
[dev, example].each { |lines| lines.push("JOBS_WORKERS=2", "JOBS_QUEUES=#{queues}") }
|
|
420
433
|
end
|
|
421
434
|
|
|
422
|
-
# REDIS_URL is
|
|
423
|
-
# actually exercises RedisFanout needs it
|
|
424
|
-
#
|
|
435
|
+
# REDIS_URL is absent from .env.test for --redis alone -- only a test
|
|
436
|
+
# that actually exercises RedisFanout needs it. --live --redis is
|
|
437
|
+
# different: config/live.rb raises without it, and the test helper
|
|
438
|
+
# loads config/live.rb through config/load.rb. Building the fanout
|
|
439
|
+
# connects to nothing, so tests still need no Redis running unless
|
|
440
|
+
# one publishes.
|
|
425
441
|
if @redis
|
|
426
442
|
dev << "REDIS_URL=redis://localhost:6379/0"
|
|
427
443
|
example << "REDIS_URL=redis://localhost:6379/0"
|
|
444
|
+
test << "REDIS_URL=redis://localhost:6379/0" if live_over_redis?
|
|
428
445
|
end
|
|
429
446
|
|
|
430
447
|
write_lines(".env", dev)
|
|
@@ -480,17 +497,21 @@ module Monk
|
|
|
480
497
|
|
|
481
498
|
- `config/live.rb` -- the Redis wiring (`Monk::WebSocket::RedisFanout`)
|
|
482
499
|
and the subscribe rules (nothing is allowed unless a rule says so).
|
|
483
|
-
- `
|
|
484
|
-
- `views/index.erb` -- `live_topic "hits"` marks what to subscribe to.
|
|
485
|
-
- `views/live/_hits.erb` -- the fragment that gets pushed. Partials
|
|
500
|
+
- `app/app.rb` -- `POST /hit` changes state and calls `Monk::Live.patch`.
|
|
501
|
+
- `app/views/index.erb` -- `live_topic "hits"` marks what to subscribe to.
|
|
502
|
+
- `app/views/live/_hits.erb` -- the fragment that gets pushed. Partials
|
|
486
503
|
used this way see only their locals (`locals[:hits]`), never
|
|
487
504
|
`params` or the session.
|
|
505
|
+
- `bin/websocket_server` -- calls `Monk::Live.listen!` at boot: the
|
|
506
|
+
only process that listens for updates. `bin/server`#{@jobs ? " and `bin/jobs`" : ""} only
|
|
507
|
+
publish.
|
|
488
508
|
- `public/js/monk_live/` -- the browser runtime, copied from the gem.
|
|
489
509
|
The layout points at it and at `LIVE_WS_URL` (default
|
|
490
510
|
`ws://localhost:9293`; use `wss://` in production).
|
|
491
511
|
|
|
492
512
|
`WS_ALLOWED_ORIGINS` (default `http://localhost:9292`) must list the
|
|
493
513
|
origin your pages are served from, or the browser's socket is refused.
|
|
514
|
+
#{live_auth_setup_paragraph}
|
|
494
515
|
MARKDOWN
|
|
495
516
|
end
|
|
496
517
|
|
|
@@ -517,18 +538,21 @@ module Monk
|
|
|
517
538
|
|
|
518
539
|
- `config/live.rb` -- the Postgres wiring (`Monk::WebSocket::PgFanout`)
|
|
519
540
|
and the subscribe rules (nothing is allowed unless a rule says so).
|
|
520
|
-
- `
|
|
521
|
-
- `views/index.erb` -- `live_topic "hits"` marks what to subscribe to.
|
|
522
|
-
- `views/live/_hits.erb` -- the fragment that gets pushed. Partials
|
|
541
|
+
- `app/app.rb` -- `POST /hit` changes state and calls `Monk::Live.patch`.
|
|
542
|
+
- `app/views/index.erb` -- `live_topic "hits"` marks what to subscribe to.
|
|
543
|
+
- `app/views/live/_hits.erb` -- the fragment that gets pushed. Partials
|
|
523
544
|
used this way see only their locals (`locals[:hits]`), never
|
|
524
545
|
`params` or the session.
|
|
546
|
+
- `bin/websocket_server` -- calls `Monk::Live.listen!` at boot: the
|
|
547
|
+
only process that listens for updates. `bin/server`#{@jobs ? " and `bin/jobs`" : ""} only
|
|
548
|
+
publish.
|
|
525
549
|
- `public/js/monk_live/` -- the browser runtime, copied from the gem.
|
|
526
550
|
The layout points at it and at `LIVE_WS_URL` (default
|
|
527
551
|
`ws://localhost:9293`; use `wss://` in production).
|
|
528
552
|
|
|
529
553
|
`WS_ALLOWED_ORIGINS` (default `http://localhost:9292`) must list the
|
|
530
554
|
origin your pages are served from, or the browser's socket is refused.
|
|
531
|
-
|
|
555
|
+
#{live_auth_setup_paragraph}
|
|
532
556
|
A single broadcast is capped at just under 8000 bytes
|
|
533
557
|
(`Monk::WebSocket::PgFanout::MAX_NOTIFY_PAYLOAD_BYTES`, Postgres's own
|
|
534
558
|
`NOTIFY` limit) -- far more than this demo ever sends, but worth
|
|
@@ -583,8 +607,7 @@ module Monk
|
|
|
583
607
|
|
|
584
608
|
ENV["MONK_ENV"] ||= "test"
|
|
585
609
|
#{base_test_dotenv_lines}
|
|
586
|
-
|
|
587
|
-
require_relative "../config/settings"#{%(\nrequire_relative "../config/mail") if @mail}
|
|
610
|
+
#{test_helper_app_lines}
|
|
588
611
|
```
|
|
589
612
|
|
|
590
613
|
**Rakefile**:
|
|
@@ -600,22 +623,7 @@ module Monk
|
|
|
600
623
|
task default: :test
|
|
601
624
|
```
|
|
602
625
|
|
|
603
|
-
|
|
604
|
-
rackup file, not a plain `.rb` a test could `require_relative`, so this
|
|
605
|
-
starts with what's actually requirable standalone. Extract `App` into
|
|
606
|
-
its own file (`require_relative`d from both `config.ru` and
|
|
607
|
-
`test/test_helper.rb`) once there's real app behavior worth testing
|
|
608
|
-
against requests:
|
|
609
|
-
|
|
610
|
-
```ruby
|
|
611
|
-
require_relative "test_helper"
|
|
612
|
-
|
|
613
|
-
class SettingsTest < Minitest::Test
|
|
614
|
-
def test_monk_env_reads_as_test
|
|
615
|
-
assert_equal "test", Monk::Settings[:monk_env]
|
|
616
|
-
end
|
|
617
|
-
end
|
|
618
|
-
```
|
|
626
|
+
#{app_test_sample}
|
|
619
627
|
|
|
620
628
|
Then:
|
|
621
629
|
|
|
@@ -656,8 +664,9 @@ module Monk
|
|
|
656
664
|
<<~MARKDOWN
|
|
657
665
|
# Setting up #{app_name} (dev, then test)
|
|
658
666
|
|
|
659
|
-
`config.
|
|
660
|
-
-- `config.
|
|
667
|
+
`config/load.rb`, `.env`, and `.env.test` are already wired up by `monk new`
|
|
668
|
+
-- `config/load.rb` requires `config/#{@auth ? "auth" : "persistence"}` (and every other config
|
|
669
|
+
you enabled), then loads `app/`, and
|
|
661
670
|
`.env`/`.env.test` are pre-filled with a database name derived from
|
|
662
671
|
this project's directory (`#{app_name}_development` / `#{app_name}_test`), not the
|
|
663
672
|
generic `app_development` fallback baked into `config/persistence.rb`
|
|
@@ -719,7 +728,7 @@ module Monk
|
|
|
719
728
|
```bash
|
|
720
729
|
bin/server # HTTP app on :9292
|
|
721
730
|
bin/websocket_server # WS chat process on :9293, in another terminal
|
|
722
|
-
#{"bin/jobs # background jobs (jobs/), in a third terminal\n" if @jobs}```
|
|
731
|
+
#{"bin/jobs # background jobs (app/jobs/), in a third terminal\n" if @jobs}```
|
|
723
732
|
#{auth_or_redis_confirmation_note}#{mail_setup_note}#{jobs_setup_note}
|
|
724
733
|
## Test environment
|
|
725
734
|
|
|
@@ -755,7 +764,7 @@ module Monk
|
|
|
755
764
|
|
|
756
765
|
**test/test_helper.rb** -- loads `.env.test` explicitly (not the default
|
|
757
766
|
dotenv-in-`config/settings.rb` path, which only loads plain `.env`), then
|
|
758
|
-
|
|
767
|
+
loads and boots the app the same way `config.ru` does:
|
|
759
768
|
|
|
760
769
|
```ruby
|
|
761
770
|
$LOAD_PATH.unshift(File.expand_path("..", __dir__))
|
|
@@ -765,9 +774,7 @@ module Monk
|
|
|
765
774
|
require "dotenv"
|
|
766
775
|
Dotenv.load(File.expand_path(".env.test", __dir__ + "/.."))
|
|
767
776
|
|
|
768
|
-
|
|
769
|
-
require_relative "../config/settings"
|
|
770
|
-
require_relative "../config/#{@auth ? "auth" : "persistence"}"#{%(\nrequire_relative "../config/mail") if @mail}#{%(\nrequire_relative "../config/jobs") if @jobs}
|
|
777
|
+
#{test_helper_app_lines}
|
|
771
778
|
```
|
|
772
779
|
|
|
773
780
|
**Rakefile**:
|
|
@@ -793,6 +800,8 @@ module Monk
|
|
|
793
800
|
#{sample_test_body}
|
|
794
801
|
end
|
|
795
802
|
```
|
|
803
|
+
|
|
804
|
+
#{app_test_sample}
|
|
796
805
|
#{jobs_test_sample}
|
|
797
806
|
Then:
|
|
798
807
|
|
|
@@ -811,6 +820,7 @@ module Monk
|
|
|
811
820
|
end
|
|
812
821
|
|
|
813
822
|
def auth_or_redis_confirmation_note
|
|
823
|
+
return live_auth_confirmation_note if @live
|
|
814
824
|
return "" unless @auth || @redis
|
|
815
825
|
|
|
816
826
|
flags = [("authenticate: true" if @auth), ("redis fan-out: on" if @redis)].compact.join(", ")
|
|
@@ -824,6 +834,34 @@ module Monk
|
|
|
824
834
|
"#{visible} visible to it -- that confirms everything's actually wired up.\n"
|
|
825
835
|
end
|
|
826
836
|
|
|
837
|
+
# Who gets which topics once there are users. Interpolated into a
|
|
838
|
+
# squiggly heredoc, so no indentation of its own.
|
|
839
|
+
def live_auth_setup_paragraph
|
|
840
|
+
return "" unless @auth
|
|
841
|
+
|
|
842
|
+
<<~MARKDOWN
|
|
843
|
+
|
|
844
|
+
**Visitors and logged-in users.** A logged-in user's socket carries
|
|
845
|
+
their identity (the session cookie reaches `:9293` too); a visitor's
|
|
846
|
+
comes in anonymous. Each topic's rule in `config/live.rb` decides:
|
|
847
|
+
`anonymous: true` opens it to visitors (the demo's `hits`), and a
|
|
848
|
+
rule without it is for logged-in users only, e.g. one topic per user.
|
|
849
|
+
`monk new` writes no login routes: see `docs/guides/auth.md` in the
|
|
850
|
+
monk repo, and `docs/guides/live.md`, "Who may subscribe".
|
|
851
|
+
MARKDOWN
|
|
852
|
+
end
|
|
853
|
+
|
|
854
|
+
# The --live bin/websocket_server prints only `authenticate:`, and with
|
|
855
|
+
# --auth it's :optional (a visitor's socket is anonymous, not refused).
|
|
856
|
+
def live_auth_confirmation_note
|
|
857
|
+
return "" unless @auth
|
|
858
|
+
|
|
859
|
+
"\n`bin/websocket_server`'s startup line should print `authenticate: optional` once " \
|
|
860
|
+
"`AUTH_SECRET` is visible to it: a logged-in user's socket carries their identity, and a " \
|
|
861
|
+
"visitor's comes in anonymous, so it gets only the topics `config/live.rb` opens with " \
|
|
862
|
+
"`anonymous: true`.\n"
|
|
863
|
+
end
|
|
864
|
+
|
|
827
865
|
# Interpolated into a squiggly heredoc, so no leading indentation of
|
|
828
866
|
# its own (interpolated text isn't dedented).
|
|
829
867
|
def mail_setup_note
|
|
@@ -854,9 +892,9 @@ module Monk
|
|
|
854
892
|
|
|
855
893
|
`config/jobs.rb` configures `Monk::Jobs` on the app's own database --
|
|
856
894
|
its tables come from `db/migrate/00000000000002_create_jobs_tables`,
|
|
857
|
-
which `bin/setup_db` already applied
|
|
858
|
-
`jobs
|
|
859
|
-
demo job and watch it run:
|
|
895
|
+
which `bin/setup_db` already applied. The job classes live in
|
|
896
|
+
`app/jobs/`, which `config/load.rb` loads. With `bin/server` and
|
|
897
|
+
`bin/jobs` both running, enqueue the demo job and watch it run:
|
|
860
898
|
|
|
861
899
|
```bash
|
|
862
900
|
curl -X POST "http://localhost:9292/jobs/hello?name=Ann"
|
|
@@ -892,7 +930,7 @@ module Monk
|
|
|
892
930
|
|
|
893
931
|
<<~MARKDOWN.chomp
|
|
894
932
|
|
|
895
|
-
Magic links go through a job too, `jobs/send_login_link.rb`: it
|
|
933
|
+
Magic links go through a job too, `app/jobs/send_login_link.rb`: it
|
|
896
934
|
creates the token and sends the link inside the job, so the raw
|
|
897
935
|
token is never stored, not even in the queue. Your login route,
|
|
898
936
|
after its own per-email rate limit, just enqueues it:
|
|
@@ -939,11 +977,12 @@ module Monk
|
|
|
939
977
|
end
|
|
940
978
|
|
|
941
979
|
# The no-Postgres test helper normally has nothing to load from
|
|
942
|
-
# .env.test -- but with --mail it has MAIL_URL=log://, which tests need
|
|
943
|
-
# they boot outside development, where an unset MAIL_URL raises
|
|
980
|
+
# .env.test -- but with --mail it has MAIL_URL=log://, which tests need
|
|
981
|
+
# (they boot outside development, where an unset MAIL_URL raises), and
|
|
982
|
+
# with --live --redis it has REDIS_URL, which config/live.rb needs.
|
|
944
983
|
# Interpolated into a squiggly heredoc, so no indentation of its own.
|
|
945
984
|
def base_test_dotenv_lines
|
|
946
|
-
return "" unless @mail
|
|
985
|
+
return "" unless @mail || live_over_redis?
|
|
947
986
|
|
|
948
987
|
%(\nrequire "dotenv"\nDotenv.load(File.expand_path(".env.test", __dir__ + "/.."))\n)
|
|
949
988
|
end
|
|
@@ -951,8 +990,8 @@ module Monk
|
|
|
951
990
|
def auth_mail_sentence
|
|
952
991
|
return "" unless @auth
|
|
953
992
|
|
|
954
|
-
" `
|
|
955
|
-
"(HTML part: `views/mail/magic_link.erb`)."
|
|
993
|
+
" `AppMailer::MAGIC_LINK` (`app/mailers/app_mailer.rb`) sends each login link through it " \
|
|
994
|
+
"(HTML part: `app/views/mail/magic_link.erb`)."
|
|
956
995
|
end
|
|
957
996
|
|
|
958
997
|
def sample_test_body
|
|
@@ -976,6 +1015,44 @@ module Monk
|
|
|
976
1015
|
end
|
|
977
1016
|
end
|
|
978
1017
|
|
|
1018
|
+
def live_over_redis? = @live && @live_transport == :redis
|
|
1019
|
+
|
|
1020
|
+
# The end of every SETUP.md test helper: the app loaded the way
|
|
1021
|
+
# config.ru loads it (config/load.rb, then app/app.rb), booted once.
|
|
1022
|
+
# Interpolated into a squiggly heredoc, so no indentation of its own.
|
|
1023
|
+
def test_helper_app_lines
|
|
1024
|
+
<<~RUBY.chomp
|
|
1025
|
+
require "minitest/autorun"
|
|
1026
|
+
require_relative "../config/load"
|
|
1027
|
+
require_relative "../app/app"
|
|
1028
|
+
|
|
1029
|
+
# Booted once, as config.ru does: request tests call APP.
|
|
1030
|
+
APP = Monk.boot(App)
|
|
1031
|
+
RUBY
|
|
1032
|
+
end
|
|
1033
|
+
|
|
1034
|
+
# A request through the booted app. Every generated app, --live's
|
|
1035
|
+
# included, has GET /hello.
|
|
1036
|
+
def app_test_sample
|
|
1037
|
+
<<~MARKDOWN.chomp
|
|
1038
|
+
**test/app_test.rb** -- a request through the booted app:
|
|
1039
|
+
|
|
1040
|
+
```ruby
|
|
1041
|
+
require_relative "test_helper"
|
|
1042
|
+
require "rack/mock_request"
|
|
1043
|
+
|
|
1044
|
+
class AppTest < Minitest::Test
|
|
1045
|
+
def test_hello
|
|
1046
|
+
status, _headers, body = APP.call(Rack::MockRequest.env_for("/hello"))
|
|
1047
|
+
|
|
1048
|
+
assert_equal 200, status
|
|
1049
|
+
assert_equal "hello from monk", body.join
|
|
1050
|
+
end
|
|
1051
|
+
end
|
|
1052
|
+
```
|
|
1053
|
+
MARKDOWN
|
|
1054
|
+
end
|
|
1055
|
+
|
|
979
1056
|
def write_file(relative_path, template_path, executable: false)
|
|
980
1057
|
destination = File.join(@dir, relative_path)
|
|
981
1058
|
FileUtils.mkdir_p(File.dirname(destination))
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# The emails this app sends, through Monk::Mail (config/mail.rb). Module
|
|
2
|
+
# constants and module methods rather than an instance, since Monk::Auth's
|
|
3
|
+
# deliver: hook must be Ractor-shareable (a lambda built at a file's top
|
|
4
|
+
# level, where self isn't, would fail) -- the same constraint as
|
|
5
|
+
# Monk::Live.authorize blocks.
|
|
6
|
+
module AppMailer
|
|
7
|
+
# Monk::Auth's deliver: hook (config/auth.rb), called by
|
|
8
|
+
# Monk::Auth.deliver_link. The HTML part is
|
|
9
|
+
# app/views/mail/magic_link.erb. Swap the body for SMS or any other
|
|
10
|
+
# channel; see docs/guides/auth.md.
|
|
11
|
+
MAGIC_LINK = lambda do |email:, link:, token:|
|
|
12
|
+
# Development only (a no-op elsewhere): the link on the console, plus a
|
|
13
|
+
# QR code for a phone if rqrcode is in the Gemfile.
|
|
14
|
+
Monk::Auth.log_dev_link(link, subject: email)
|
|
15
|
+
Monk::Mail.deliver(
|
|
16
|
+
to: email,
|
|
17
|
+
subject: "Your login link",
|
|
18
|
+
text: "Here's your login link:\n\n#{link}\n\nIf you didn't ask for it, you can ignore this email.",
|
|
19
|
+
html: Monk::Mail.render("mail/magic_link", link: link),
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -7,25 +7,12 @@ require_relative "persistence"
|
|
|
7
7
|
# "#{Monk::Settings[:public_url]}/auth/callback/#{token}"), see its comment
|
|
8
8
|
# there and auth.md's "Sending the magic link".
|
|
9
9
|
|
|
10
|
-
# Sends the magic link by email
|
|
11
|
-
#
|
|
12
|
-
# Auth.configure
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
|
|
16
|
-
module AppMailer
|
|
17
|
-
DELIVER = lambda do |email:, link:, token:|
|
|
18
|
-
# Development only (a no-op elsewhere): the link on the console, plus a
|
|
19
|
-
# QR code for a phone if rqrcode is in the Gemfile.
|
|
20
|
-
Monk::Auth.log_dev_link(link, subject: email)
|
|
21
|
-
Monk::Mail.deliver(
|
|
22
|
-
to: email,
|
|
23
|
-
subject: "Your login link",
|
|
24
|
-
text: "Here's your login link:\n\n#{link}\n\nIf you didn't ask for it, you can ignore this email.",
|
|
25
|
-
html: Monk::Mail.render("mail/magic_link", link: link),
|
|
26
|
-
)
|
|
27
|
-
end
|
|
28
|
-
end
|
|
10
|
+
# Sends the magic link by email: AppMailer::MAGIC_LINK, in
|
|
11
|
+
# app/mailers/app_mailer.rb. Required here, not left to config/load.rb:
|
|
12
|
+
# Monk::Auth.configure checks deliver: right away, and bin/websocket_server
|
|
13
|
+
# loads this file without config/load.rb. The mailer only touches
|
|
14
|
+
# Monk::Mail when it's called, so that process needs no mail config.
|
|
15
|
+
require_relative "../app/mailers/app_mailer"
|
|
29
16
|
|
|
30
17
|
Monk::Auth.configure(
|
|
31
18
|
db_name: :primary,
|
|
@@ -34,5 +21,11 @@ Monk::Auth.configure(
|
|
|
34
21
|
session_ttl: 1_209_600, # seconds a session stays valid (14 days)
|
|
35
22
|
redirect_allowlist: [], # paths request_login(redirect_to:) is allowed to target
|
|
36
23
|
secure: !Monk.env.development?, # Secure cookie flag; off in dev so plain-http testing works (Safari drops it)
|
|
37
|
-
deliver: AppMailer::
|
|
24
|
+
deliver: AppMailer::MAGIC_LINK,
|
|
38
25
|
)
|
|
26
|
+
|
|
27
|
+
# bin/websocket_server checks each socket's session through this pool, so
|
|
28
|
+
# it holds 4 connections however many pages are open, instead of one per
|
|
29
|
+
# open page. Declaring it opens nothing: only bin/websocket_server starts
|
|
30
|
+
# it. See docs/guides/persistence.md, "Pools", in the monk repo.
|
|
31
|
+
Monk::Persistence::Pg.pool(:auth, size: 4)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -48,7 +48,20 @@ module ChatServer
|
|
|
48
48
|
end
|
|
49
49
|
end
|
|
50
50
|
|
|
51
|
-
|
|
51
|
+
# This process holds the sockets, so it's the one that listens for other
|
|
52
|
+
# processes' broadcasts: with REDIS_URL, a subscriber connection, opened
|
|
53
|
+
# here at boot (an unreachable Redis fails right here). A plain Registry
|
|
54
|
+
# has nothing to listen to, and listen! does nothing.
|
|
55
|
+
ChatServer::REGISTRY.listen!
|
|
56
|
+
|
|
57
|
+
# false, not nil, without --auth: Server.new refuses nil.
|
|
58
|
+
authenticate = !!(defined?(Monk::Auth) && Monk::Auth.config)
|
|
59
|
+
# Sessions are checked through config/auth.rb's :auth pool rather than in
|
|
60
|
+
# each socket's own Ractor, which would hold a Postgres connection per
|
|
61
|
+
# open page. This is the only process that uses the pool, so it starts it.
|
|
62
|
+
db_pool = (:auth if authenticate)
|
|
63
|
+
Monk::Persistence::Pg.start_pools!(db_pool) if db_pool
|
|
64
|
+
|
|
52
65
|
port = ENV.fetch("WS_PORT", "9293").to_i
|
|
53
66
|
# Defaults to public_url (config/settings.rb) so one PUBLIC_URL env var
|
|
54
67
|
# keeps this in sync with the app's actual origin instead of two vars
|
|
@@ -56,7 +69,9 @@ port = ENV.fetch("WS_PORT", "9293").to_i
|
|
|
56
69
|
# more than one origin (comma-separated).
|
|
57
70
|
allowed_origins = ENV.fetch("WS_ALLOWED_ORIGINS", Monk::Settings[:public_url]).split(",")
|
|
58
71
|
|
|
59
|
-
server = Monk::WebSocket::Server.new(
|
|
72
|
+
server = Monk::WebSocket::Server.new(
|
|
73
|
+
port: port, authenticate: authenticate, allowed_origins: allowed_origins, db_pool: db_pool,
|
|
74
|
+
)
|
|
60
75
|
puts "Monk::WebSocket::Server listening on ws://0.0.0.0:#{port} " \
|
|
61
76
|
"(authenticate: #{authenticate}, redis fan-out: #{ENV["REDIS_URL"] ? "on" : "off"})"
|
|
62
77
|
server.run(&ChatServer::HANDLER)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Loads everything the app runs on: the settings, each Monk module's config,
|
|
2
|
+
# then the app's own code under app/. Required by config.ru and the tests;
|
|
3
|
+
# it never boots the app itself (that's config.ru's Monk.boot).
|
|
4
|
+
require_relative "settings"
|
|
5
|
+
|
|
6
|
+
# The app's code, one directory per role, in the order they call each
|
|
7
|
+
# other: presenters read models, helpers, mailers and broadcasts use both,
|
|
8
|
+
# and jobs call all of them. Everything is loaded here, before Monk.boot
|
|
9
|
+
# freezes the app -- nothing can be loaded later from a worker Ractor.
|
|
10
|
+
#
|
|
11
|
+
# The order only matters for code that runs while a file loads (a
|
|
12
|
+
# superclass, a constant in a class body, a Monk::Context.include): such a
|
|
13
|
+
# file require_relative's what it needs first. Code inside methods runs
|
|
14
|
+
# later, so it may use any role, even one loaded after it. See Monk's
|
|
15
|
+
# docs/guides/scaffolding.md, "Where code goes".
|
|
16
|
+
%w[models presenters helpers mailers broadcasts jobs].each do |role|
|
|
17
|
+
Dir[File.expand_path("../app/#{role}/**/*.rb", __dir__)].sort.each { |file| require file }
|
|
18
|
+
end
|
|
@@ -1,13 +1,4 @@
|
|
|
1
|
-
require_relative "config/
|
|
1
|
+
require_relative "config/load" # the configs and app/ (config/load.rb)
|
|
2
|
+
require_relative "app/app"
|
|
2
3
|
|
|
3
|
-
|
|
4
|
-
views "views"
|
|
5
|
-
layout "layouts/app"
|
|
6
|
-
assets "public"
|
|
7
|
-
|
|
8
|
-
get("/") { @title = "App"; render "index" }
|
|
9
|
-
get("/hello") { "hello from monk" }
|
|
10
|
-
get("/api/hello") { json(message: "hello from monk") }
|
|
11
|
-
end
|
|
12
|
-
|
|
13
|
-
run Monk.boot(App)
|
|
4
|
+
run Monk.boot(App) # Boot: freezes the app and serves it
|