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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +581 -0
- data/LICENSE.txt +21 -0
- data/README.md +88 -0
- data/exe/monk +116 -0
- data/lib/monk/assets.rb +197 -0
- data/lib/monk/auth/errors.rb +13 -0
- data/lib/monk/auth/helpers.rb +76 -0
- data/lib/monk/auth/login_token.rb +11 -0
- data/lib/monk/auth/rate_limiter.rb +46 -0
- data/lib/monk/auth/session.rb +11 -0
- data/lib/monk/auth.rb +301 -0
- data/lib/monk/base.rb +323 -0
- data/lib/monk/context.rb +78 -0
- data/lib/monk/environment.rb +50 -0
- data/lib/monk/errors.rb +37 -0
- data/lib/monk/freeze_hooks.rb +23 -0
- data/lib/monk/live/client/idiomorph.LICENSE +13 -0
- data/lib/monk/live/client/idiomorph.js +4 -0
- data/lib/monk/live/client/monk_live.js +204 -0
- data/lib/monk/live/client/protocol.js +87 -0
- data/lib/monk/live/envelope.rb +51 -0
- data/lib/monk/live/errors.rb +9 -0
- data/lib/monk/live/helpers.rb +22 -0
- data/lib/monk/live/policy.rb +58 -0
- data/lib/monk/live/publisher.rb +91 -0
- data/lib/monk/live/renderer.rb +47 -0
- data/lib/monk/live/session.rb +121 -0
- data/lib/monk/live.rb +96 -0
- data/lib/monk/log.rb +130 -0
- data/lib/monk/persistence/errors.rb +7 -0
- data/lib/monk/persistence/model.rb +41 -0
- data/lib/monk/persistence/pg/errors.rb +4 -0
- data/lib/monk/persistence/pg/migrator.rb +165 -0
- data/lib/monk/persistence/pg/model.rb +233 -0
- data/lib/monk/persistence/pg.rb +34 -0
- data/lib/monk/persistence.rb +113 -0
- data/lib/monk/scaffold.rb +606 -0
- data/lib/monk/settings.rb +151 -0
- data/lib/monk/state_ractor.rb +45 -0
- data/lib/monk/templates/auth/config/auth.rb +28 -0
- data/lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.down.sql +2 -0
- data/lib/monk/templates/auth/db/migrate/00000000000001_create_auth_tables.up.sql +18 -0
- data/lib/monk/templates/base/.dockerignore +5 -0
- data/lib/monk/templates/base/.gitignore +4 -0
- data/lib/monk/templates/base/.ruby-version +1 -0
- data/lib/monk/templates/base/Dockerfile +28 -0
- data/lib/monk/templates/base/Gemfile +7 -0
- data/lib/monk/templates/base/bin/server +5 -0
- data/lib/monk/templates/base/bin/websocket_server +62 -0
- data/lib/monk/templates/base/config/settings.rb +30 -0
- data/lib/monk/templates/base/config.ru +13 -0
- data/lib/monk/templates/base/public/css/app.css +17 -0
- data/lib/monk/templates/base/public/js/app.js +5 -0
- data/lib/monk/templates/base/views/index.erb +6 -0
- data/lib/monk/templates/base/views/layouts/app.erb +18 -0
- data/lib/monk/templates/live/bin/websocket_server +30 -0
- data/lib/monk/templates/live/config/live.rb +47 -0
- data/lib/monk/templates/live/config.ru +27 -0
- data/lib/monk/templates/live/views/index.erb +18 -0
- data/lib/monk/templates/live/views/live/_hits.erb +1 -0
- data/lib/monk/templates/postgres/Dockerfile +30 -0
- data/lib/monk/templates/postgres/Gemfile.extra +2 -0
- data/lib/monk/templates/postgres/bin/console +7 -0
- data/lib/monk/templates/postgres/bin/migrate +22 -0
- data/lib/monk/templates/postgres/bin/setup_db +9 -0
- data/lib/monk/templates/postgres/config/persistence.rb +10 -0
- data/lib/monk/templates/redis/Gemfile.extra +1 -0
- data/lib/monk/version.rb +9 -0
- data/lib/monk/views.rb +175 -0
- data/lib/monk/websocket/connection.rb +226 -0
- data/lib/monk/websocket/errors.rb +9 -0
- data/lib/monk/websocket/frame.rb +71 -0
- data/lib/monk/websocket/handshake.rb +77 -0
- data/lib/monk/websocket/redis_fanout.rb +103 -0
- data/lib/monk/websocket/registry.rb +92 -0
- data/lib/monk/websocket/server.rb +234 -0
- data/lib/monk/websocket.rb +19 -0
- data/lib/monk.rb +45 -0
- 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.
|