monkrb 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +581 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +88 -0
  5. data/exe/monk +116 -0
  6. data/lib/monk/assets.rb +197 -0
  7. data/lib/monk/auth/errors.rb +13 -0
  8. data/lib/monk/auth/helpers.rb +76 -0
  9. data/lib/monk/auth/login_token.rb +11 -0
  10. data/lib/monk/auth/rate_limiter.rb +46 -0
  11. data/lib/monk/auth/session.rb +11 -0
  12. data/lib/monk/auth.rb +301 -0
  13. data/lib/monk/base.rb +323 -0
  14. data/lib/monk/context.rb +78 -0
  15. data/lib/monk/environment.rb +50 -0
  16. data/lib/monk/errors.rb +37 -0
  17. data/lib/monk/freeze_hooks.rb +23 -0
  18. data/lib/monk/live/client/idiomorph.LICENSE +13 -0
  19. data/lib/monk/live/client/idiomorph.js +4 -0
  20. data/lib/monk/live/client/monk_live.js +204 -0
  21. data/lib/monk/live/client/protocol.js +87 -0
  22. data/lib/monk/live/envelope.rb +51 -0
  23. data/lib/monk/live/errors.rb +9 -0
  24. data/lib/monk/live/helpers.rb +22 -0
  25. data/lib/monk/live/policy.rb +58 -0
  26. data/lib/monk/live/publisher.rb +91 -0
  27. data/lib/monk/live/renderer.rb +47 -0
  28. data/lib/monk/live/session.rb +121 -0
  29. data/lib/monk/live.rb +96 -0
  30. data/lib/monk/log.rb +130 -0
  31. data/lib/monk/persistence/errors.rb +7 -0
  32. data/lib/monk/persistence/model.rb +41 -0
  33. data/lib/monk/persistence/pg/errors.rb +4 -0
  34. data/lib/monk/persistence/pg/migrator.rb +165 -0
  35. data/lib/monk/persistence/pg/model.rb +233 -0
  36. data/lib/monk/persistence/pg.rb +34 -0
  37. data/lib/monk/persistence.rb +113 -0
  38. data/lib/monk/scaffold.rb +606 -0
  39. data/lib/monk/settings.rb +151 -0
  40. data/lib/monk/state_ractor.rb +45 -0
  41. data/lib/monk/templates/auth/config/auth.rb +28 -0
  42. data/lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.down.sql +2 -0
  43. data/lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.up.sql +18 -0
  44. data/lib/monk/templates/base/.dockerignore +5 -0
  45. data/lib/monk/templates/base/.gitignore +4 -0
  46. data/lib/monk/templates/base/.ruby-version +1 -0
  47. data/lib/monk/templates/base/Dockerfile +28 -0
  48. data/lib/monk/templates/base/Gemfile +7 -0
  49. data/lib/monk/templates/base/bin/server +5 -0
  50. data/lib/monk/templates/base/bin/websocket_server +62 -0
  51. data/lib/monk/templates/base/config/settings.rb +30 -0
  52. data/lib/monk/templates/base/config.ru +13 -0
  53. data/lib/monk/templates/base/public/css/app.css +17 -0
  54. data/lib/monk/templates/base/public/js/app.js +5 -0
  55. data/lib/monk/templates/base/views/index.erb +6 -0
  56. data/lib/monk/templates/base/views/layouts/app.erb +18 -0
  57. data/lib/monk/templates/live/bin/websocket_server +30 -0
  58. data/lib/monk/templates/live/config/live.rb +47 -0
  59. data/lib/monk/templates/live/config.ru +27 -0
  60. data/lib/monk/templates/live/views/index.erb +18 -0
  61. data/lib/monk/templates/live/views/live/_hits.erb +1 -0
  62. data/lib/monk/templates/postgres/Dockerfile +30 -0
  63. data/lib/monk/templates/postgres/Gemfile.extra +2 -0
  64. data/lib/monk/templates/postgres/bin/console +7 -0
  65. data/lib/monk/templates/postgres/bin/migrate +22 -0
  66. data/lib/monk/templates/postgres/bin/setup_db +9 -0
  67. data/lib/monk/templates/postgres/config/persistence.rb +10 -0
  68. data/lib/monk/templates/redis/Gemfile.extra +1 -0
  69. data/lib/monk/version.rb +9 -0
  70. data/lib/monk/views.rb +175 -0
  71. data/lib/monk/websocket/connection.rb +226 -0
  72. data/lib/monk/websocket/errors.rb +9 -0
  73. data/lib/monk/websocket/frame.rb +71 -0
  74. data/lib/monk/websocket/handshake.rb +77 -0
  75. data/lib/monk/websocket/redis_fanout.rb +103 -0
  76. data/lib/monk/websocket/registry.rb +92 -0
  77. data/lib/monk/websocket/server.rb +234 -0
  78. data/lib/monk/websocket.rb +19 -0
  79. data/lib/monk.rb +45 -0
  80. metadata +252 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 7fc84b47997c81eb2748a3326ae5adba796ae90eb143d3c537b2f718d41b4a33
4
+ data.tar.gz: 7f2072700db0787f7e9ee8bb0c28bf03e09b6194d2ad3c8aedcb89490c179ad8
5
+ SHA512:
6
+ metadata.gz: f55f23d4954fec6400c1976a00f203bbd4b17df3ab1c0b0150051fd64a85db5e8e5f43229593a4d23333e56a4051fb39a45e2e9a9ce20dd0f3780101e918427a
7
+ data.tar.gz: 94aad3a80714da837196a0c81d1191dd5c9e5490ebb7ed5094b2fb737dcb55615d4f187d13876acd125830ce14bf46ab9f6718654d7ff2b0435bfc108bd214ea
data/CHANGELOG.md ADDED
@@ -0,0 +1,581 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. Format is loosely
4
+ [Keep a Changelog](https://keepachangelog.com/); versions are as released
5
+ in `lib/monk/version.rb`.
6
+
7
+ ## 0.15.0 - 2026-09-22
8
+
9
+ ### Changed
10
+
11
+ - **Published to RubyGems as `monkrb`**: the `monk` name on rubygems.org
12
+ belongs to an unrelated, long-abandoned gem, so the gemspec is renamed
13
+ `monk.gemspec` -> `monkrb.gemspec` with `spec.name = "monkrb"` — the code
14
+ namespace and `require "monk"` are unchanged. Added `source_code_uri`,
15
+ `changelog_uri`, `bug_tracker_uri`, and `rubygems_mfa_required` gemspec
16
+ metadata. `monk new`'s scaffolded `Gemfile` now declares `gem "monkrb",
17
+ require: "monk"` instead of `gem "monk"`. `docs/guides/deploying.md` and
18
+ the README's Quick Start (`gem install monkrb`) updated to match.
19
+ - README's Status paragraph no longer points at issue #19 as the living
20
+ roadmap (closed, empty backlog) and now lists deployment support and the
21
+ RubyGems release among what's done.
22
+
23
+ ## 0.14.0 - 2026-09-22
24
+
25
+ ### Added
26
+
27
+ - `Monk::Auth.configure(secure:)` (default `true`): set `false` to omit the
28
+ `Secure` flag from the session and CSRF cookies, so login works over plain
29
+ `http://` (Safari drops `Secure` cookies there). The `monk new --auth`
30
+ scaffold sets it to `!Monk.env.development?`.
31
+ - `Monk::Auth.configure(deliver:)` and `Monk::Auth.deliver_link(email:,
32
+ link:, token:)`: a single call for an app's login route to send the magic
33
+ link, picking the configured `deliver:` callable when set, falling back to
34
+ `Monk::Auth.log_dev_link` in development, and raising
35
+ `Monk::MissingAuthDeliveryError` otherwise instead of silently doing
36
+ nothing. `deliver:` must be Ractor-shareable, same constraint as a route
37
+ block. See `docs/guides/auth.md`, "Sending the magic link".
38
+ - `monk new` scaffolds a `Dockerfile` and `.dockerignore` into every app
39
+ (two-stage build, no compiler needed since `kino` ships as precompiled
40
+ platform gems). `--postgres`/`--auth` swap in a variant that adds
41
+ `libpq-dev`/`libpq5` for the `pg` gem's native extension. Default `CMD`
42
+ runs `bin/server --bind 0.0.0.0` on port 9292; run the same image with
43
+ `bin/websocket_server` as the command for the WebSocket process. See
44
+ `docs/guides/deploying.md`.
45
+ - `config/settings.rb` (every `monk new` app) declares a `public_url`
46
+ setting, this app's own trusted origin (`PUBLIC_URL` env var, default
47
+ `http://localhost:9292`). `--auth`'s magic link, `--live`'s `live_ws_url`,
48
+ and `bin/websocket_server`'s `WS_ALLOWED_ORIGINS` now all default from it
49
+ instead of three separately hardcoded values that could drift out of
50
+ sync — setting `PUBLIC_URL` alone keeps them consistent. `live_ws_url`
51
+ defaults to the direct `ws://localhost:9293` port in development, or a
52
+ `wss://`/`/ws` path under `public_url` outside it, matching the
53
+ reverse-proxy routing `docs/guides/deploying.md` section 3 sets up. See
54
+ `docs/guides/live.md`, "Running it".
55
+
56
+ ### Fixed
57
+
58
+ - `docs/guides/deploying.md`'s Fly.io Dockerfile snippet ran the app's HTTP
59
+ server on port 9293 — colliding with `WS_PORT`'s own default of 9293 for
60
+ the separate WebSocket process. Corrected to 9292, matching `bin/server`'s
61
+ actual default.
62
+ - `docs/guides/deploying.md`'s "Before the first build" claimed a
63
+ Mac-generated `Gemfile.lock` only lists `arm64-darwin` and needs
64
+ `bundle lock --add-platform` before a Linux build. Verified false for
65
+ this project's pinned toolchain (Ruby 4.0.6/Bundler 4.0.16): a plain
66
+ `bundle install` already resolves and locks every compatible platform.
67
+ Removed the now-incorrect instruction.
68
+
69
+ ## 0.13.0 - 2026-09-21
70
+
71
+ ### Added
72
+
73
+ - **`Monk::Live`** (`require "monk/live"`, opt-in): push server-rendered HTML
74
+ to open browser tabs. `Monk::Live.patch/append/prepend/remove/batch`
75
+ render a `Monk::Views` partial once and broadcast it (through a
76
+ `Monk::WebSocket::Registry`, or a `RedisFanout` across processes) to a
77
+ topic's subscribers; `Monk::Live::HANDLER` serves the sockets, with
78
+ deny-by-default `Monk::Live.authorize` rules; the `live_topic` view helper
79
+ marks what a page subscribes to; and a browser runtime (shipped in the gem,
80
+ `Monk::Live.client_dir`, vendoring idiomorph for the DOM morph) applies
81
+ patches while keeping focus and typed text, reconnects, and re-syncs by
82
+ refetching the page. See `docs/guides/live.md`.
83
+ - **`monk new APP --live`**: scaffolds a Monk::Live demo (a counter whose
84
+ open tabs update together). Implies `--redis`.
85
+
86
+ ### Fixed
87
+
88
+ - **`Monk::WebSocket::Frame.encode` raised `Encoding::CompatibilityError` for
89
+ any UTF-8 text containing non-ASCII characters** (`"caffè"`, `"☃"`, an
90
+ emoji): the frame header is a BINARY string holding a non-ASCII byte and
91
+ can't be concatenated with such a payload. Only ASCII text and BINARY
92
+ payloads (which is all a client message ever is, so echoing chat worked)
93
+ were sendable. The payload is now sent as bytes. Found by the Monk::Live
94
+ cross-process tests, where a fragment with an accent silently never
95
+ arrived.
96
+
97
+ ### Changed
98
+
99
+ - **Docs reorganized**: plans, notes and one-off analyses moved to
100
+ `docs/history/`, usage guides to `docs/guides/`, design docs to
101
+ `docs/design/`; the README Status now states the project is pre-1.0.
102
+
103
+ ## 0.12.5 - 2026-09-18
104
+
105
+ ### Added
106
+
107
+ - **`Monk::Auth.log_dev_link(link, subject: nil)`** (`lib/monk/auth.rb`):
108
+ in development only, prints a magic link to stdout and the dev log, plus a
109
+ scannable QR code beneath it when the app's own Gemfile includes the
110
+ optional `rqrcode` gem. A no-op outside development.
111
+
112
+ ### Fixed
113
+
114
+ - **`log_dev_link`'s QR code raised `Ractor::IsolationError` inside a worker
115
+ Ractor**: `rqrcode` has unfrozen lookup-table constants that can't be read
116
+ from a non-main Ractor. `Monk::Auth` now walks the `RQRCode` /
117
+ `RQRCodeCore` constants at boot and makes them shareable.
118
+ - **`log_dev_link`'s QR code was too big for the terminal**: `as_ansi` spends
119
+ two columns and one line per module. It is now rendered with half-block
120
+ characters (two module rows per line, one column per module, black on white
121
+ so it stays scannable on dark terminals) at error-correction level `:l`,
122
+ roughly a quarter of the previous area.
123
+
124
+ ## 0.12.4 - 2026-09-17
125
+
126
+ ### Fixed
127
+
128
+ - **Test suite still printed `Warning: no type cast defined for type
129
+ "regclass"...` on every migrator test** (`test/persistence_migrator_test.rb`):
130
+ `SELECT to_regclass('widgets')` returns a `regclass`-typed column, and
131
+ `lib/monk/persistence/pg.rb`'s `PG::BasicTypeMapForResults` has no
132
+ decoder registered for it, so the `pg` gem warns once per call and falls
133
+ back to the raw string. Added `PersistenceTestHelpers#table_exists?`
134
+ (`test/test_helper.rb`) — the same `information_schema.tables` check
135
+ `Migrator#ensure_schema_migrations_table` and `#drop_table_if_exists`
136
+ already use, which never touches `regclass` at all — and replaced every
137
+ `to_regclass` assertion with it.
138
+
139
+ ## 0.12.3 - 2026-09-17
140
+
141
+ ### Fixed
142
+
143
+ - **Ctrl+C with no live connections open still printed an unrescued
144
+ `Interrupt` stack trace** (`lib/monk/websocket/server.rb`): 0.12.1 only
145
+ rescued `Ractor::ClosedError` in `Registry#ask`, which guards a
146
+ connection's cleanup racing the registry Ractor's teardown -- a
147
+ different failure point from this one. `Server#run`'s accept loop
148
+ itself never rescued Ctrl+C's default `Interrupt`, raised in whatever
149
+ thread is blocked in `TCPServer#accept` -- unrescued, that's an
150
+ unhandled exception with a backtrace, even though stopping the server
151
+ this way is normal and intended. `#run` now rescues `Interrupt` around
152
+ the loop and closes the `TCPServer`.
153
+
154
+ ## 0.12.2 - 2026-09-17
155
+
156
+ ### Fixed
157
+
158
+ - **Test suite printed a "method redefined" warning per view per test file**
159
+ (`lib/monk/views.rb`): `Views.method_name_for` deliberately reuses the
160
+ same compiled method name across repeated boots of the same template
161
+ path (so a second `.freeze!` in one process redefines in place rather
162
+ than growing new method names forever), but Ruby warns on every `def`
163
+ that overwrites an existing method unless the old one was explicitly
164
+ removed first. `Views.compile` now calls `Compiled.remove_method` ahead
165
+ of the redefinition it already intends, silencing the warning without
166
+ changing behavior.
167
+ - **Test suite printed a Postgres NOTICE ("table ... does not exist,
168
+ skipping") on nearly every test** (`test/test_helper.rb` and the
169
+ persistence/auth/migrator tests): every test's `teardown` already drops
170
+ its own tables, so the *next* test's `DROP TABLE IF EXISTS` setup call
171
+ almost always hits the non-existent case. Added
172
+ `PersistenceTestHelpers#drop_table_if_exists`, which checks
173
+ `information_schema.tables` first and only issues `DROP TABLE` when the
174
+ table is actually there — the same dodge `Migrator#
175
+ ensure_schema_migrations_table` already uses for `CREATE TABLE IF NOT
176
+ EXISTS`'s NOTICE, in the other direction. Replaced every raw `DROP TABLE
177
+ IF EXISTS` in the test suite with it.
178
+
179
+ ## 0.12.1 - 2026-09-17
180
+
181
+ ### Fixed
182
+
183
+ - **Ctrl+C with live WebSocket connections open printed an unrescued
184
+ `Ractor::ClosedError` stack trace** (`lib/monk/websocket/registry.rb`):
185
+ `Registry#ask` (backing `register`/`unregister`/`broadcast`/`count`)
186
+ sends to the registry's own dedicated Ractor with no guard, unlike
187
+ `Registry#broadcast`'s inner loop, which already rescues
188
+ `Ractor::ClosedError` per-port for the same class of shutdown race
189
+ (with a comment describing exactly this). On SIGINT, Ruby tears down
190
+ Ractors with no defined order; if the registry's Ractor is gone before
191
+ a connection's `Server.serve`-ensure cleanup calls `unsubscribe!` ->
192
+ `unregister` -> `ask`, the send raises unrescued and `report_on_exception`
193
+ prints it. Harmless — the process is already exiting, no message loss
194
+ or leaked state — but noisy. `#ask` now rescues `Ractor::ClosedError`
195
+ and returns `nil`, which every caller here already ignores or only
196
+ reads while the registry is known to be alive.
197
+
198
+ ## 0.12.0 - 2026-09-17
199
+
200
+ ### Added
201
+
202
+ - **Timestamps on every log line** (`lib/monk/log.rb`, `lib/monk/base.rb`):
203
+ both the per-request access line (`Base#log_request`, to `$stdout` and
204
+ `log/<env>.log`) and the app-level `Monk::Log.debug`/`.info`/`.warn`/
205
+ `.error` lines now lead with a UTC, millisecond-precision ISO 8601 stamp
206
+ (`2026-09-17T14:32:01.123Z GET /hello -> 200 (1.2ms)`,
207
+ `2026-09-17T14:32:01.456Z WARN payment retried`), via a single
208
+ `Monk::Log.timestamp` used by both call sites so the two log lines stay
209
+ in the same format. `Time.now` needs no Ractor-shareability handling —
210
+ it returns a fresh, unshared value on every call — so this added no new
211
+ boot-time freezing concerns.
212
+
213
+ ## 0.11.3 - 2026-09-15
214
+
215
+ ### Fixed
216
+
217
+ - **`Monk::Log.info`/`debug`/`warn`/`error` raised `Ractor::IsolationError`
218
+ from a worker Ractor** (`lib/monk/log.rb`): the level methods were
219
+ defined via `define_method(&block)`, and a method backed by a Proc
220
+ closure can't be called from a Ractor other than the one that defined
221
+ it — `Environment`'s own comment already warns against exactly this
222
+ pattern (found there first, for `MONK_ENV_VALUES`), but `Log`'s level
223
+ methods didn't follow it. Kino runs each request in a worker Ractor, so
224
+ every call from request-handling code hit this. Replaced with four
225
+ plain `def` methods, same fix as `Environment`. Fixing that surfaced a
226
+ second bug behind it: `Settings::LOG_LEVEL_VALUES` (aliased as
227
+ `Log::LEVELS`, read on every `#enabled?` call) was only shallow-frozen
228
+ via `Array#freeze`, which doesn't freeze the strings inside and so
229
+ isn't Ractor-shareable either — switched to `Ractor.make_shareable`.
230
+ Found via `monk_talk`'s real usage, not by inspection.
231
+
232
+ ## 0.11.2 - 2026-09-15
233
+
234
+ ### Fixed
235
+
236
+ - **`monk new --postgres`'s generated `.env` was silently never loaded**
237
+ (`lib/monk/scaffold.rb`): `.env`/`.env.test` have shipped with real
238
+ values since 0.11.0, but `dotenv` stayed commented out in the `Gemfile`,
239
+ so `config/settings.rb`'s `require "dotenv/load"` never actually ran —
240
+ `bin/setup_db` and friends fell straight back to
241
+ `config/persistence.rb`'s own `ENV.fetch` defaults instead of the
242
+ app-specific values `.env` was written to provide. `--postgres` now
243
+ uncomments `gem "dotenv"` in the `Gemfile` too. Found the same way as
244
+ 0.11.0/0.11.1 — generating a real app and following its own `SETUP.md`,
245
+ not by inspection.
246
+ - **`--redis` alone (no `--postgres`) had none of the above** — no `.env`
247
+ at all, so `REDIS_URL` had to be exported by hand for
248
+ `bin/websocket_server`'s fan-out to turn on. `write_env_files!` and the
249
+ `dotenv` fix above now both run whenever `@postgres || @redis`, not just
250
+ `@postgres`; `write_env_files!` only adds the Postgres lines when
251
+ `@postgres` is actually set, and skips writing `.env.test` entirely when
252
+ it would end up empty (true for `--redis` alone, since `REDIS_URL` is
253
+ deliberately never written there anyway — only a test that actually
254
+ exercises `RedisFanout` needs it). `SETUP.md`'s base-skeleton content
255
+ (also covers `--redis`-only apps) now describes the real `.env`/`dotenv`/
256
+ Redis-container setup instead of telling you to export `REDIS_URL`
257
+ inline. Verified end-to-end: a fresh `--redis`-only app's
258
+ `bin/websocket_server` printed `redis fan-out: on` against a real Redis
259
+ with zero manual exports, purely from the generated `.env` + `dotenv`.
260
+
261
+ ## 0.11.1 - 2026-09-15
262
+
263
+ ### Fixed
264
+
265
+ - **`monk new` now generates `SETUP.md` for every flag combination, not
266
+ just `--postgres`** (`lib/monk/scaffold.rb`, `exe/monk`): 0.11.0's
267
+ `SETUP.md` generation was gated behind `@postgres`, so the base skeleton
268
+ and `--redis`-only apps got no setup instructions at all, despite both
269
+ still having a real first dev step (`bin/server`) and an unscaffolded
270
+ test framework to wire up. `setup_md_content` now branches on
271
+ `@postgres` between the existing Postgres-oriented walkthrough and a new
272
+ lightweight one for base/`--redis`-only apps — no database/container/
273
+ migration steps, and a Minitest smoke test against `Monk::Settings`
274
+ instead of `Persistence::Pg`, since `config.ru`'s `class App` lives
275
+ inline in a rackup file with nothing else standalone-requirable to test
276
+ yet.
277
+ - **README Quick Start's `bundle exec rake test` line**: a plain
278
+ `monk new my_app` (no flags) has never scaffolded a `Rakefile` or
279
+ `test/` directory, so that command has been broken since it was first
280
+ written (#36) — unrelated to the `SETUP.md` fix above, but caught and
281
+ fixed alongside it.
282
+
283
+ ## 0.11.0 - 2026-09-15
284
+
285
+ ### Added
286
+
287
+ - **`monk new --postgres` now wires the generated app together instead of
288
+ just dropping files next to each other** (`lib/monk/scaffold.rb`,
289
+ `exe/monk`): previously `config.ru` never required `config/persistence.rb`
290
+ or `config/auth.rb` at all, so a freshly scaffolded HTTP process never
291
+ actually registered a database connection unless you edited `config.ru`
292
+ by hand.
293
+ - `config.ru` now gets `require_relative "config/persistence"` (or
294
+ `"config/auth"` under `--auth` — `config/auth.rb` itself
295
+ `require_relative`s `persistence`) appended right after the settings
296
+ require, before `class App`.
297
+ - `.env`, `.env.test`, and a tracked `.env.example` are generated with
298
+ `DB_NAME` derived from the target directory name
299
+ (`APP_NAME_development`/`APP_NAME_test`), instead of relying on the
300
+ generic `app_development` fallback baked into `config/persistence.rb`'s
301
+ own `ENV.fetch` — every scaffolded app used to default to that same
302
+ literal name, risking collisions between separate local apps sharing
303
+ one Postgres instance. `--auth` adds a placeholder `AUTH_SECRET` to all
304
+ three files; `--redis` adds a placeholder `REDIS_URL` to `.env`/
305
+ `.env.example` only, deliberately not `.env.test` — only a test that
306
+ actually exercises `RedisFanout` needs it.
307
+ - A generated `SETUP.md`, tailored to the exact flags passed, walks
308
+ through dev setup (reusing or starting Postgres/Redis containers,
309
+ creating the database, running migrations, booting `bin/server`/
310
+ `bin/websocket_server`) and then test setup, including the minimum
311
+ Minitest wiring (`test/test_helper.rb`, a `Rakefile`, one real smoke
312
+ test) since `monk new` still scaffolds no test framework itself.
313
+
314
+ ## 0.10.0 - 2026-09-09
315
+
316
+ ### Added
317
+
318
+ - **`Monk::Log` level methods** (`lib/monk/log.rb`, #46): `.debug`/`.info`/
319
+ `.warn`/`.error` for app-level logging, each writing one `LEVEL message`
320
+ line to the existing `log/<env>.log` when at or above the configured
321
+ threshold, a no-op otherwise. Distinct from `#write`, `Log`'s existing
322
+ unconditional per-request access-log line, which no threshold gates.
323
+ - **`LOG_LEVEL` setting** (`lib/monk/settings.rb`, #46): a new
324
+ `Settings`-backed key, implicit like `MONK_ENV` (no app `configure`
325
+ call needed), one of `debug`/`info`/`warn`/`error` (`info` default),
326
+ validated at `Boot` and resolved once into `Log`'s threshold rather
327
+ than read per call.
328
+
329
+ ## 0.9.0 - 2026-09-09
330
+
331
+ ### Added
332
+
333
+ - **WebSocket cross-process fan-out over Redis** (`Monk::WebSocket::RedisFanout`,
334
+ `lib/monk/websocket/redis_fanout.rb`, #43): wraps a `Registry` with the
335
+ identical `#register`/`#unregister`/`#count`/`#broadcast` interface, so
336
+ swapping which object a connection holds is the only change an app
337
+ makes. `#broadcast` still delivers to this process's own `Registry`
338
+ directly (same latency and reliability as a plain `Registry`, even if
339
+ Redis is briefly down) and publishes to Redis so sibling
340
+ `Monk::WebSocket::Server` processes relay it to their own connections;
341
+ every publish carries a per-instance origin id so a process's own
342
+ publish, echoed back to it over Redis, is dropped instead of delivered
343
+ twice. Chosen over Postgres `LISTEN`/`NOTIFY` — no payload cap, higher
344
+ throughput, and the `redis` gem's pub/sub client is Ractor-ready.
345
+ - **`monk new` scaffolds `bin/websocket_server` unconditionally, plus a new
346
+ `--redis` opt-in** (#43): unlike `--postgres`/`--redis`, plain WebSocket
347
+ needs no external service, so it isn't gated behind a flag — every
348
+ scaffolded app gets a working `bin/websocket_server` (a single `:chat`
349
+ broadcast channel) that adapts at boot instead: `authenticate: true`
350
+ automatically if `--auth`'s `config/auth.rb` is present, `RedisFanout`
351
+ instead of a plain `Registry` automatically if `REDIS_URL` is set.
352
+ `--redis` (`monk new my_app --redis`) is fully independent of
353
+ `--postgres`/`--auth` and adds only a `Gemfile` line — there's no
354
+ `config/redis.rb`, since `REDIS_URL` is read directly the same way
355
+ `WS_PORT`/`WS_ALLOWED_ORIGINS` already are.
356
+
357
+ ### Fixed
358
+
359
+ - **`RedisFanout` wasn't actually usable the way it needed to be** (#43),
360
+ found by running it against a real Redis rather than trusting the
361
+ design: it held a live `Redis` client directly on an unfrozen instance
362
+ and never called `freeze`, so it wasn't `Ractor.shareable?` at all
363
+ (`Ractor::IsolationError` the moment a connection Ractor read it from a
364
+ module constant, the same way `Registry` already is); `CHANNEL_PREFIX`
365
+ was an unfrozen String constant, hit by the same error from inside the
366
+ subscriber Ractor; and the subscriber recovered a Redis channel name as
367
+ a String and broadcast with it directly, silently landing on a
368
+ different `Hash` key than the Symbol every real caller registers under
369
+ — `#broadcast` returned `true` while delivering to nobody. Fixed with a
370
+ lazy, per-Ractor publisher (mirrors `Monk::Persistence::Registry`),
371
+ `freeze` at the end of `#initialize` (mirrors `Registry`), and `.to_sym`
372
+ on the recovered channel name.
373
+
374
+ ## 0.8.0 - 2026-09-07
375
+
376
+ ### Added
377
+
378
+ - **`Base.resources`** (experimental, `lib/monk/base.rb`, #35): registers
379
+ the seven conventional REST routes (index/new/create/show/edit/update/
380
+ destroy) for a resource in one call, each dispatching to
381
+ `controller.new(context).public_send(action)`. Built entirely on the
382
+ existing `get`/`post`/`put`/`patch`/`delete`, so it doesn't touch
383
+ routing, dispatch, or `freeze!` — purely additive.
384
+ - **File-based request logging** (`lib/monk/log.rb`, #34): every request
385
+ now appends a line to `log/<env>.log`, Rails-style (development/test/
386
+ staging/production each get their own file), unconditionally across
387
+ all environments. The existing `$stdout` echo (with its per-line
388
+ flush) stays gated to development, unchanged.
389
+ - **`--auth` scaffold opt-in** (`monk new my_app --auth`, #36): scaffolds
390
+ `config/auth.rb` and a migration creating the `login_tokens`/`sessions`
391
+ tables `Monk::Auth` needs. Implies `--postgres` — Auth is
392
+ Postgres-only, so this always brings the persistence scaffold with it.
393
+ - `monk new` now also scaffolds `bin/server` and a project `.gitignore`
394
+ (`/log/`, `.env`/`.env.*`, keeping `.env.example`) as part of the base
395
+ skeleton.
396
+ - **`Pg::Model.where`**: accepts comparison operators (`gt:`/`gte:`/
397
+ `lt:`/`lte:`/`ne:`) and `column: [v1, v2]` for `IN`, plus a trailing
398
+ options `Hash` for `order:`/`limit:` — previously equality-only. `OR`
399
+ stays out of scope; composite conditions still need a raw `Pg.checkout`
400
+ block.
401
+ - **`Pg::Model.find_all`/`.create_all`**: `find_all(ids)` does one
402
+ `SELECT ... WHERE id IN (...)` round trip, returning rows positionally
403
+ matched to `ids` (`nil` for any missing, consistent with `find`).
404
+ `create_all(rows)` does one multi-row `INSERT ... VALUES ...
405
+ RETURNING *`; every row must share the same set of keys.
406
+ - **WebSocket `Server` heartbeat and reverification** (#39, #40):
407
+ `ping_interval:` spawns a per-connection thread sending an unsolicited
408
+ ping on that cadence, so a spec-compliant client's automatic pong
409
+ resets a reverse proxy's idle timer even on an otherwise-idle browser
410
+ connection. `reverify_interval:` (requires `authenticate: true`)
411
+ re-runs `Monk::Auth.verify` against the same credential on that
412
+ cadence and closes the connection (RFC 6455 code 1008) the moment
413
+ verify comes back `nil`. Both default to `nil` (off).
414
+ - **WebSocket `Connection` fragmentation reassembly and a payload cap**
415
+ (#41): `#read` now reassembles a fragmented message (`fin: false`
416
+ starts it, opcode `0x0` continues it, `fin: true` ends it) instead of
417
+ returning the first fragment as if it were the whole message, closing
418
+ with 1002 on an out-of-sequence fragment. `max_payload_size:` (1 MiB
419
+ default, always on) rejects any single frame — or a fragmented
420
+ message's reassembled total — over the cap with 1009, checked before
421
+ ever attempting to buffer it.
422
+ - `Monk.boot` now logs a startup line reporting the running environment,
423
+ route count, and (only when actually configured) `auth=on` and which
424
+ `Persistence::Registry` backends have registered connections;
425
+ `Persistence::Registry` gained a public `#names` reader backing that.
426
+
427
+ ### Fixed
428
+
429
+ - **WebSocket `Registry#broadcast`** (#38): a port that closed without
430
+ ever going through `#unregister` (e.g. its owning connection Ractor
431
+ died first) raised `Ractor::ClosedError` unguarded inside the
432
+ registry Ractor's own loop, killing delivery for every key in the
433
+ whole process, not just the dead connection's one. `#broadcast` now
434
+ skips a closed port, keeps delivering to the rest of that key's
435
+ subscribers, and drops the dead port so it isn't retried.
436
+
437
+ ### Changed
438
+
439
+ - **Error classes reorganized by subsystem** (#42): each subsystem's
440
+ error classes now live in a single `errors.rb` next to the code that
441
+ raises them (`auth/`, `persistence/`, `persistence/pg/`, `websocket/`),
442
+ rather than 19 one-class-per-file error files at the `lib/monk/` root.
443
+ Errors shared by single-file subsystems (settings, views/templates,
444
+ scaffold, Ractor-sharing) land in a shared `lib/monk/errors.rb`.
445
+
446
+ ## 0.7.0 - 2026-09-05
447
+
448
+ ### Added
449
+
450
+ - **Settings** (`lib/monk/settings.rb`, #32): `Monk::Settings.configure`
451
+ declares required/optional keys via a small DSL; `Settings[:key]` reads
452
+ each key's value from `ENV` (uppercased name) or its default.
453
+ Declaring the same key twice, or reading one nobody declared, raises a
454
+ precise error (`DuplicateSettingError`/`UnknownSettingError`) instead
455
+ of silently overwriting or returning `nil`. Joins `Monk.freeze_hooks`:
456
+ `Base#freeze!` validates every required key is present and freezes the
457
+ resolved values into a `Ractor.shareable?` snapshot
458
+ (`MissingSettingError` otherwise; `SettingsFrozenError` on a
459
+ post-Boot `#configure`). `Context#settings` exposes the same frozen
460
+ reads to routes.
461
+ - `MONK_ENV` is now a first-class Settings key, implicitly declared
462
+ and validated at Boot against a fixed four-value set
463
+ (development/test/staging/production) — an invalid value raises
464
+ `InvalidMonkEnvError`. `Monk.env` returns a frozen `Environment`
465
+ value object with `.development?`/`.test?`/`.staging?`/
466
+ `.production?` predicates, replacing ad hoc `ENV["MONK_ENV"] ==
467
+ "production"` checks in assets and request logging.
468
+ - `monk new` scaffolds now ship `config/settings.rb` (loads `dotenv`
469
+ if present, otherwise a no-op) required ahead of the app class body
470
+ and the `--postgres` `bin/` scripts.
471
+ - Verified under real concurrent Ractor workers, including an
472
+ end-to-end proof in `monk-consumer-test` under a multi-worker `kino`
473
+ pool.
474
+ - Routes are now indexed at Boot for O(1) static dispatch (#33):
475
+ `freeze!` builds a static verb/path hash for exact-match routes and a
476
+ per-verb array of dynamic routes with segments precomputed, instead of
477
+ `find_route` linearly re-splitting every registered route's path on
478
+ every request. Params hashes are now only allocated once a route is
479
+ confirmed to match. `bin/benchmark_router` (manual) shows ~210x on a
480
+ static route, ~31x on a 404 miss, and ~5x on a dynamic route at a
481
+ 300-route table.
482
+
483
+ ## 0.6.0 - 2026-09-05
484
+
485
+ ### Added
486
+
487
+ - **WebSockets** (`lib/monk/websocket.rb`, opt-in via
488
+ `require "monk/websocket"`): a hand-rolled RFC 6455 implementation —
489
+ handshake and frame codec (all three length encodings; `ProtocolError`
490
+ on truncated/malformed frames), a connection-per-Ractor server
491
+ (`Monk::WebSocket::Server.new(port:, bind:)`) that moves each accepted
492
+ socket into its own Ractor via `Ractor#send(..., move: true)` so one
493
+ slow or crashing connection never blocks the accept loop or any other
494
+ connection, and a full close handshake (server- and client-initiated,
495
+ plus abrupt-disconnect handling) reported to app code as a plain `nil`
496
+ read.
497
+ - `Registry`: an in-process broadcast Ractor (`register`/`unregister`/
498
+ `broadcast`) that connections subscribe to by key; broadcasts relay
499
+ onto each subscriber's socket from a background thread inside that
500
+ connection's own Ractor, with unconditional unsubscribe in `ensure`
501
+ so no lifecycle path leaks a stale entry.
502
+ - `Server.new(authenticate: true)` extracts identity via
503
+ `Monk::Auth` — `Authorization: Bearer` or the `session_token`
504
+ cookie — with `allowed_origins:` origin checking for
505
+ cookie-derived credentials; ping/pong is answered automatically
506
+ without reaching app code.
507
+ - Verified under real concurrent Ractor workers (ordinary, slow, and
508
+ crashing connections running simultaneously; broadcast from a
509
+ fourth, independent Ractor to multiple real subscribers).
510
+
511
+ ### Fixed
512
+
513
+ - `Monk.freeze!` is now callable independent of `Base#freeze!`: a
514
+ WebSocket-only app never touches `Monk::Base`, so nothing was freezing
515
+ `Monk::Auth`'s config and the first `Monk::Auth.verify` call from a
516
+ connection Ractor raised `Ractor::IsolationError`. `Server.new
517
+ (authenticate: true)` now freezes on first use, mirroring `Base.call`.
518
+
519
+ ## 0.5.0 - 2026-09-05
520
+
521
+ ### Added
522
+
523
+ - **Views** (`lib/monk/views.rb`): ERB templates under `views/` (or a path
524
+ set via `views`), compiled once at Boot into instance methods on a
525
+ module `Context` includes — never at request time, since a worker
526
+ Ractor can hold no template cache and must not install methods on a
527
+ shared module. A broken template fails Boot naming file and line.
528
+ `<%= %>` HTML-escapes by default (a deliberate break from stock ERB);
529
+ `raw(x)` opts out, `h(x)` escapes explicitly without double-escaping.
530
+ Layouts are Ruby's own `yield`, applied to the outermost `render` of a
531
+ request only. Data reaches a template as ivars set in the route and as
532
+ a `locals` hash passed to `render`.
533
+ - **Static assets** (`lib/monk/assets.rb`): `public/` is walked at Boot
534
+ into a frozen manifest (body, content-type, ETag) served before routes
535
+ for `GET`/`HEAD`. Production serves an exact-match fetch from that
536
+ manifest (path traversal is structurally impossible, not defended
537
+ against); development re-reads from disk per request instead, with a
538
+ containment check. ETag/304 support, `must-revalidate` by default, and
539
+ a `?v=` digest stamp from `asset_path` that earns a year of immutable
540
+ caching.
541
+ - **Auth** (`lib/monk/auth.rb` and friends, #28): passwordless login —
542
+ token issuance and single-use atomic redemption, session verification
543
+ and revocation, rate limiting, and cookie/redirect support — wired
544
+ through Boot and real Ractor workers.
545
+ - `monk new` now scaffolds a working HTML page (layout, index template,
546
+ stylesheet, ES-module entry point) instead of a bare JSON route.
547
+
548
+ ### Fixed
549
+
550
+ - `Monk::Assets::TEXT_TYPES`/`BINARY_TYPES` were `{ ... }.freeze`, which
551
+ freezes the hash but not the strings inside it, so the constants were
552
+ never actually `Ractor.shareable?`. In development, every static-asset
553
+ request calls `content_type` from the serving worker Ractor, which
554
+ raised `Ractor::IsolationError` on the very first request. Fixed by
555
+ building both hashes with `Ractor.make_shareable` instead.
556
+
557
+ ## 0.4.0 - Phase 4
558
+
559
+ - `monk new` / `Monk::Scaffold`: scaffolds a new project's skeleton
560
+ (`Gemfile`, `config.ru`, `.ruby-version`, `views/`, `public/`, and
561
+ `--postgres` for persistence/migrations wiring) from static templates.
562
+
563
+ ## 0.3.0 - Phase 5
564
+
565
+ - `Monk::Persistence::Pg::Migrator`: plain-SQL migrations
566
+ (`<version>_<name>.up.sql` / `.down.sql`), tracked in a
567
+ `schema_migrations` table, run explicitly (never hooked into Boot).
568
+
569
+ ## 0.2.0
570
+
571
+ - `Monk::Persistence::Pg` and `Monk::Persistence::Pg::Model`: opt-in raw
572
+ Postgres access and CRUD sugar over plain Symbol-keyed hashes, frozen
573
+ and made Ractor-shareable at Boot alongside routes and error handlers.
574
+
575
+ ## 0.1.0 (#18)
576
+
577
+ - Packaged Monk as a local gem: routing (`get`/`post`/`put`/`patch`/`delete`
578
+ with path params and a trailing splat), `Context`, `error` handlers,
579
+ `Monk.boot`/`.freeze!` sealing the app into a `Ractor.shareable?`
580
+ structure, and `Monk::StateRactor` for state that must be shared and
581
+ mutated safely across Ractor workers.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Matteo Folin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.