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
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
require_relative "../pg"
|
|
2
|
+
require_relative "../model"
|
|
3
|
+
|
|
4
|
+
module Monk
|
|
5
|
+
module Persistence
|
|
6
|
+
module Pg
|
|
7
|
+
# Deliberately not an ORM: no associations, no validations, no
|
|
8
|
+
# callbacks, no dirty-tracking, no live row objects. Every method
|
|
9
|
+
# takes or returns plain Hashes (Symbol-keyed), so nothing here has
|
|
10
|
+
# to cross a Ractor boundary as anything but copyable data.
|
|
11
|
+
class Model < Monk::Persistence::Model
|
|
12
|
+
# Maps a `where` comparison-operator key to its SQL operator.
|
|
13
|
+
# Deliberately a small, closed set -- not a general expression DSL.
|
|
14
|
+
COMPARISON_OPERATORS = {
|
|
15
|
+
gt: ">",
|
|
16
|
+
gte: ">=",
|
|
17
|
+
lt: "<",
|
|
18
|
+
lte: "<=",
|
|
19
|
+
ne: "<>",
|
|
20
|
+
}.freeze
|
|
21
|
+
|
|
22
|
+
class << self
|
|
23
|
+
def create(data)
|
|
24
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
25
|
+
columns = data.keys.map { |c| conn.quote_ident(c.to_s) }
|
|
26
|
+
placeholders = (1..data.size).map { |i| "$#{i}" }
|
|
27
|
+
sql = "INSERT INTO #{conn.quote_ident(table_name)} (#{columns.join(", ")}) " \
|
|
28
|
+
"VALUES (#{placeholders.join(", ")}) RETURNING *"
|
|
29
|
+
to_row(conn.exec_params(sql, data.values))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# One INSERT, one round trip, one implicit transaction (a single
|
|
34
|
+
# SQL statement is atomic on its own -- all rows land or none
|
|
35
|
+
# do). Every Hash must have the same set of keys; order within
|
|
36
|
+
# each Hash doesn't matter, but a differing key set would mean
|
|
37
|
+
# a differing column list per row, which a single VALUES clause
|
|
38
|
+
# can't express -- raises rather than silently NULL-filling the
|
|
39
|
+
# gap, which would mask a caller bug. Row order in the result
|
|
40
|
+
# matches `rows`' order in every version of Postgres this has
|
|
41
|
+
# been checked against, but that isn't a documented SQL-standard
|
|
42
|
+
# guarantee for multi-row RETURNING -- don't rely on it holding
|
|
43
|
+
# across an exotic BEFORE INSERT trigger.
|
|
44
|
+
def create_all(rows)
|
|
45
|
+
return [] if rows.empty?
|
|
46
|
+
|
|
47
|
+
columns = rows.first.keys
|
|
48
|
+
unless rows.all? { |row| row.keys.map(&:to_s).sort == columns.map(&:to_s).sort }
|
|
49
|
+
raise ArgumentError, "create_all requires every row to have the same columns"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
53
|
+
quoted_columns = columns.map { |c| conn.quote_ident(c.to_s) }
|
|
54
|
+
values = []
|
|
55
|
+
row_placeholders = rows.map do |row|
|
|
56
|
+
placeholders = columns.map do |column|
|
|
57
|
+
values << row.fetch(column)
|
|
58
|
+
"$#{values.size}"
|
|
59
|
+
end
|
|
60
|
+
"(#{placeholders.join(", ")})"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
sql = "INSERT INTO #{conn.quote_ident(table_name)} (#{quoted_columns.join(", ")}) " \
|
|
64
|
+
"VALUES #{row_placeholders.join(", ")} RETURNING *"
|
|
65
|
+
to_rows(conn.exec_params(sql, values))
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def find(id)
|
|
70
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
71
|
+
sql = "SELECT * FROM #{conn.quote_ident(table_name)} WHERE id = $1"
|
|
72
|
+
to_row(conn.exec_params(sql, [id]))
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# One round trip, positionally matched to `ids` -- same length,
|
|
77
|
+
# same order, a `nil` in place of any id that doesn't exist
|
|
78
|
+
# (mirrors `find`'s single-row "nil means missing" rather than
|
|
79
|
+
# silently dropping the position, which would desync a caller
|
|
80
|
+
# zipping the result back against `ids`). A duplicate id in the
|
|
81
|
+
# input appears at every one of its positions in the output.
|
|
82
|
+
def find_all(ids)
|
|
83
|
+
return [] if ids.empty?
|
|
84
|
+
|
|
85
|
+
rows = where(id: ids)
|
|
86
|
+
by_id = rows.each_with_object({}) { |row, h| h[row[:id]] = row }
|
|
87
|
+
ids.map { |id| by_id[id] }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# AND-only -- still no OR, no arbitrary boolean trees. A condition
|
|
91
|
+
# value is either a scalar (equality), an Array (IN -- an empty
|
|
92
|
+
# Array matches no rows rather than producing invalid SQL), or a
|
|
93
|
+
# Hash of comparison-operator => operand (see
|
|
94
|
+
# COMPARISON_OPERATORS; multiple keys on one column AND together,
|
|
95
|
+
# e.g. `quantity: { gte: 1, lt: 10 }`). An empty conditions Hash
|
|
96
|
+
# means no filter (all rows), not an error: the natural
|
|
97
|
+
# degenerate case of zero AND'd conditions.
|
|
98
|
+
#
|
|
99
|
+
# `options[:order]` is a column Symbol/String (ascending) or a
|
|
100
|
+
# Hash of column => :asc/:desc for more than one column.
|
|
101
|
+
# `options[:limit]` is an Integer row cap. Both are optional and
|
|
102
|
+
# independent of conditions.
|
|
103
|
+
#
|
|
104
|
+
# `options` is a plain trailing Hash, not `order:`/`limit:`
|
|
105
|
+
# keyword parameters -- Ruby only lets a bare `where(col: val)`
|
|
106
|
+
# call collapse into the `conditions` positional Hash (the
|
|
107
|
+
# calling convention every existing caller uses) as long as
|
|
108
|
+
# `where` declares no real keyword parameters of its own; adding
|
|
109
|
+
# `order:`/`limit:` as keywords would make Ruby try to parse
|
|
110
|
+
# `col: val` as keyword arguments instead and raise.
|
|
111
|
+
def where(conditions, options = {})
|
|
112
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
113
|
+
clause, values = where_clause(conn, conditions)
|
|
114
|
+
|
|
115
|
+
sql = +"SELECT * FROM #{conn.quote_ident(table_name)}"
|
|
116
|
+
sql << " WHERE #{clause}" if clause
|
|
117
|
+
sql << order_clause(conn, options[:order]) if options[:order]
|
|
118
|
+
if (limit = options[:limit])
|
|
119
|
+
raise ArgumentError, "limit must be an Integer, got #{limit.inspect}" unless limit.is_a?(Integer)
|
|
120
|
+
|
|
121
|
+
values << limit
|
|
122
|
+
sql << " LIMIT $#{values.size}"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
to_rows(conn.exec_params(sql, values))
|
|
126
|
+
end
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def update(id, data)
|
|
130
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
131
|
+
sets = data.keys.each_with_index
|
|
132
|
+
.map { |c, i| "#{conn.quote_ident(c.to_s)} = $#{i + 2}" }.join(", ")
|
|
133
|
+
sql = "UPDATE #{conn.quote_ident(table_name)} SET #{sets} WHERE id = $1 RETURNING *"
|
|
134
|
+
to_row(conn.exec_params(sql, [id, *data.values]))
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Conditional UPDATE ... RETURNING * -- the row or nil, in one
|
|
139
|
+
# atomic statement instead of read-then-update, so two concurrent
|
|
140
|
+
# claims of the same row can't both succeed (see Model.update's
|
|
141
|
+
# unconditional `WHERE id = $1`, which can't express this guard).
|
|
142
|
+
# Equality + AND only, consistent with `where`: a `nil` condition
|
|
143
|
+
# value maps to `IS NULL`, not to a bound `= NULL` (which would
|
|
144
|
+
# never match, since SQL NULL comparisons aren't true/false).
|
|
145
|
+
def claim(conditions, data)
|
|
146
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
147
|
+
sets = data.keys.each_with_index
|
|
148
|
+
.map { |c, i| "#{conn.quote_ident(c.to_s)} = $#{i + 1}" }.join(", ")
|
|
149
|
+
|
|
150
|
+
bound, nil_conditions = conditions.partition { |_, v| !v.nil? }
|
|
151
|
+
where_parts = bound.each_with_index.map { |(c, _), i| "#{conn.quote_ident(c.to_s)} = $#{data.size + i + 1}" }
|
|
152
|
+
where_parts += nil_conditions.map { |c, _| "#{conn.quote_ident(c.to_s)} IS NULL" }
|
|
153
|
+
|
|
154
|
+
sql = "UPDATE #{conn.quote_ident(table_name)} SET #{sets} " \
|
|
155
|
+
"WHERE #{where_parts.join(" AND ")} RETURNING *"
|
|
156
|
+
to_row(conn.exec_params(sql, data.values + bound.map(&:last)))
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def delete(id)
|
|
161
|
+
Monk::Persistence::Pg.checkout(db_name) do |conn|
|
|
162
|
+
sql = "DELETE FROM #{conn.quote_ident(table_name)} WHERE id = $1"
|
|
163
|
+
conn.exec_params(sql, [id]).cmd_tuples.positive?
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
private
|
|
168
|
+
|
|
169
|
+
# Returns [clause_string_or_nil, bound_values]. Building the
|
|
170
|
+
# values array alongside the clause (rather than a separate
|
|
171
|
+
# pass) keeps each condition's placeholder index in lockstep
|
|
172
|
+
# with where it lands in `values`, including the multi-operator
|
|
173
|
+
# Hash case where one column contributes more than one param.
|
|
174
|
+
def where_clause(conn, conditions)
|
|
175
|
+
values = []
|
|
176
|
+
parts = conditions.map do |column, condition|
|
|
177
|
+
ident = conn.quote_ident(column.to_s)
|
|
178
|
+
case condition
|
|
179
|
+
when Hash
|
|
180
|
+
condition.map { |op, operand| comparison(ident, op, operand, values) }.join(" AND ")
|
|
181
|
+
when Array
|
|
182
|
+
in_clause(ident, condition, values)
|
|
183
|
+
else
|
|
184
|
+
values << condition
|
|
185
|
+
"#{ident} = $#{values.size}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
[parts.empty? ? nil : parts.join(" AND "), values]
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
def comparison(ident, op, operand, values)
|
|
192
|
+
operator = COMPARISON_OPERATORS.fetch(op) do
|
|
193
|
+
raise ArgumentError, "unsupported where operator #{op.inspect}"
|
|
194
|
+
end
|
|
195
|
+
values << operand
|
|
196
|
+
"#{ident} #{operator} $#{values.size}"
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def in_clause(ident, list, values)
|
|
200
|
+
return "1 = 0" if list.empty?
|
|
201
|
+
|
|
202
|
+
placeholders = list.map do |item|
|
|
203
|
+
values << item
|
|
204
|
+
"$#{values.size}"
|
|
205
|
+
end
|
|
206
|
+
"#{ident} IN (#{placeholders.join(", ")})"
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def order_clause(conn, order)
|
|
210
|
+
columns = order.is_a?(Hash) ? order : { order => :asc }
|
|
211
|
+
clause = columns.map do |column, direction|
|
|
212
|
+
direction = direction.to_sym
|
|
213
|
+
unless %i[asc desc].include?(direction)
|
|
214
|
+
raise ArgumentError, "order direction must be :asc or :desc, got #{direction.inspect}"
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
"#{conn.quote_ident(column.to_s)} #{direction.to_s.upcase}"
|
|
218
|
+
end.join(", ")
|
|
219
|
+
" ORDER BY #{clause}"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def to_rows(result)
|
|
223
|
+
result.map { |row| row.transform_keys(&:to_sym) }
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def to_row(result)
|
|
227
|
+
to_rows(result).first
|
|
228
|
+
end
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
end
|
|
232
|
+
end
|
|
233
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
require "pg"
|
|
2
|
+
|
|
3
|
+
require_relative "../persistence"
|
|
4
|
+
|
|
5
|
+
module Monk
|
|
6
|
+
module Persistence
|
|
7
|
+
# Postgres backend. Opt-in: require "monk/persistence/pg" explicitly --
|
|
8
|
+
# `require "monk"` alone does not load this. Each Ractor lazily opens
|
|
9
|
+
# and memoizes its own PG::Connection on first access, never shared
|
|
10
|
+
# across Ractors (mirrors pg's own documented Ractor pattern --
|
|
11
|
+
# PG::Connection is explicitly not shareable and must be created fresh
|
|
12
|
+
# per Ractor). Concurrent access from sibling threads within the same
|
|
13
|
+
# Ractor is serialized through #checkout (from Registry), since a bare
|
|
14
|
+
# PG::Connection isn't safe for two threads to issue commands on at
|
|
15
|
+
# once.
|
|
16
|
+
module Pg
|
|
17
|
+
extend Monk::Persistence::Registry
|
|
18
|
+
|
|
19
|
+
class << self
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def connect(**opts)
|
|
23
|
+
conn = PG.connect(**opts)
|
|
24
|
+
conn.type_map_for_results = PG::BasicTypeMapForResults.new(conn)
|
|
25
|
+
conn
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def disconnect(conn)
|
|
29
|
+
conn.finish
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
require_relative "freeze_hooks"
|
|
2
|
+
|
|
3
|
+
module Monk
|
|
4
|
+
# Backend-agnostic persistence. Concrete backends (e.g.
|
|
5
|
+
# Monk::Persistence::Pg, loaded separately -- persistence backends are
|
|
6
|
+
# opt-in, not required by `require "monk"`) extend Registry below and
|
|
7
|
+
# register themselves into Monk.freeze_hooks automatically, so Base#freeze!
|
|
8
|
+
# can seal every backend actually in use without needing to know their
|
|
9
|
+
# names.
|
|
10
|
+
module Persistence
|
|
11
|
+
# Shared by every backend module: per-Ractor connection lifecycle, a
|
|
12
|
+
# registry of named configs, and boot-time shareability sealing. A
|
|
13
|
+
# backend `extend`s this and implements #connect(**opts) /
|
|
14
|
+
# #disconnect(conn) (both private); registration, lookup, serialized
|
|
15
|
+
# checkout, and freezing are identical across backends and live here
|
|
16
|
+
# once.
|
|
17
|
+
module Registry
|
|
18
|
+
DEFAULT_CHECKOUT_TIMEOUT = 5 # seconds
|
|
19
|
+
|
|
20
|
+
Entry = Struct.new(:conn, :slot)
|
|
21
|
+
|
|
22
|
+
def self.extended(base)
|
|
23
|
+
Monk.freeze_hooks << base
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def register(name, **opts)
|
|
27
|
+
configs[name] = opts
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
# Registered connection names (e.g. :primary) -- Monk.boot's log
|
|
31
|
+
# line reads this to report which backends an app actually uses.
|
|
32
|
+
def names
|
|
33
|
+
configs.keys
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def [](name)
|
|
37
|
+
entry(name).conn
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def checkout(name, timeout: DEFAULT_CHECKOUT_TIMEOUT)
|
|
41
|
+
e = entry(name)
|
|
42
|
+
token = e.slot.pop(timeout: timeout)
|
|
43
|
+
if token.nil?
|
|
44
|
+
raise Monk::PersistenceTimeoutError,
|
|
45
|
+
"timed out waiting for the #{name.inspect} connection " \
|
|
46
|
+
"(checkout held longer than #{timeout}s)"
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
yield e.conn
|
|
50
|
+
ensure
|
|
51
|
+
e.slot << true if token
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Called from Base#freeze! (Seam B), via Monk.freeze_hooks.
|
|
55
|
+
# Without this, #register'd configs are unreachable from any worker
|
|
56
|
+
# Ractor at all: @configs is a plain, unfrozen Hash, and reading an
|
|
57
|
+
# unfrozen value from a class/module instance variable raises
|
|
58
|
+
# Ractor::IsolationError from any non-main Ractor -- the same
|
|
59
|
+
# restriction Model.freeze_all! exists for, just on the
|
|
60
|
+
# connect-options registry instead of a Model's own config. Freezing
|
|
61
|
+
# the value (not the module) fixes it, the same way it did there.
|
|
62
|
+
def freeze_registry!
|
|
63
|
+
@configs = Ractor.make_shareable(configs)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Test-only: drops all registered configs and this Ractor's cached
|
|
67
|
+
# connections. Not part of the app-facing API.
|
|
68
|
+
def reset!
|
|
69
|
+
Ractor.current[:monk_persistence]&.each_value do |e|
|
|
70
|
+
disconnect(e.conn)
|
|
71
|
+
rescue StandardError
|
|
72
|
+
end
|
|
73
|
+
Ractor.current[:monk_persistence] = {}
|
|
74
|
+
@configs = {}
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def configs
|
|
80
|
+
@configs ||= {}
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def ractor_local
|
|
84
|
+
Ractor.current[:monk_persistence] ||= {}
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def entry(name)
|
|
88
|
+
ractor_local[name] ||= build_entry(name)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def build_entry(name)
|
|
92
|
+
opts = configs.fetch(name) do
|
|
93
|
+
raise Monk::UnknownPersistenceError,
|
|
94
|
+
"no database registered as #{name.inspect} -- call " \
|
|
95
|
+
"#{self}.register(#{name.inspect}, ...) first"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
slot = SizedQueue.new(1)
|
|
99
|
+
slot << true
|
|
100
|
+
|
|
101
|
+
Entry.new(connect(**opts), slot)
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def connect(**opts)
|
|
105
|
+
raise NotImplementedError, "#{self} must implement #connect(**opts)"
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def disconnect(conn)
|
|
109
|
+
raise NotImplementedError, "#{self} must implement #disconnect(conn)"
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|