Skip to content

Commit c2326f8

Browse files
committed
Implement module reloading on file change
1 parent f16cc8e commit c2326f8

5 files changed

Lines changed: 56 additions & 134 deletions

File tree

.rubocop.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,8 @@ AllCops:
3535
# Style/MultilineBlockChain:
3636
# Enabled: false
3737

38-
# Lint/RescueException:
39-
# Enabled: false
38+
Lint/RescueException:
39+
Enabled: false
4040

4141
# Lint/InheritException:
4242
# Enabled: false

TODO.md

Lines changed: 0 additions & 97 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +0,0 @@
1-
## Server tool
2-
3-
```bash
4-
$ bundle exec syntropy --dev ./site
5-
$ bundle exec syntropy --workers 4 ./site
6-
```
7-
8-
And also a config file:
9-
10-
```bash
11-
$ bundle exec syntropy site.rb
12-
```
13-
14-
And the config file:
15-
16-
```ruby
17-
# site.rb
18-
Syntropy.config do
19-
root './site'
20-
workers 4
21-
log { |req| }
22-
end
23-
```
24-
25-
## Lightweight model API on top of Extralite
26-
27-
- DB connection pool
28-
- Lightweight means 90% features with 10% effort:
29-
30-
```ruby
31-
Posts = Syntropy::Relation.new('posts')
32-
33-
posts = Posts.order_by(:stamp, :desc).all(db)
34-
35-
post = Posts.where(id: 13).first(db)
36-
37-
id = Posts.insert(db, title: 'foo', body: 'bar')
38-
39-
Posts.where(id: id).update(db, body: 'baz')
40-
41-
Posts.where(id: id).delete(db)
42-
```
43-
44-
The whole `db` argument thing is very limiting. For easier usage we integrate
45-
the db connection pool as dependency injection the model:
46-
47-
```ruby
48-
db_pool = E2::ConnectionPool.new(fn)
49-
Posts = Syntropy::Dataset.new(db_pool, 'posts')
50-
51-
Posts[id: 1] #=> { id: 1, title: 'foo', body: 'bar' }
52-
Posts.find(id: 1) #=>
53-
54-
Posts.to_a #=> [...]
55-
Posts.order_by(:stamp, :desc).to_a #=> [...]
56-
57-
id = Posts.insert(title: 'foo', body: 'bar')
58-
59-
post = Posts.where(id: id)
60-
post.values #=> { id: 1, title: 'foo', body: 'bar' }
61-
post.update(body: 'baz') #=> 1
62-
post.delete
63-
```
64-
65-
So basically it's a bit similar to Sequel datasets, but there's no "object instance as single row". The instance is the entire set of rows in the table, or a subset thereof:
66-
67-
```ruby
68-
Posts.where(...).order_by(...).select(...).from(rowset)
69-
```
70-
71-
How about CTEs?
72-
73-
```ruby
74-
Users = Syntrop::Dataset.new(db_pool, 'users')
75-
76-
GroupIdRowset = Syntropy::Dataset {
77-
with(
78-
foo: Users,
79-
bar: -> {
80-
select user_id, group
81-
from foo
82-
},
83-
baz: -> {
84-
select id
85-
from bar
86-
where user_id == bar.select(:user_id)
87-
}
88-
)
89-
90-
select_all
91-
from baz
92-
where id == :group_id
93-
}
94-
95-
users = GroupIdRowset.bind(group_id: 5).to_a
96-
97-
```

bin/syntropy

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,10 @@ if !File.directory?(opts[:location])
5656
exit
5757
end
5858

59+
60+
5961
opts[:machine] = UM.new
60-
app = Syntropy::App.new(opts[:machine], opts[:location], '/', watch_files: 0.05)
61-
TP2.run(opts) { app.(it) }
62+
opts[:logger] = opts[:logger] && TP2::Logger.new(opts[:machine], **opts)
63+
64+
app = Syntropy::App.new(opts[:machine], opts[:location], '/', opts)
65+
TP2.run(opts) { app.call(it) }

lib/syntropy/app.rb

Lines changed: 47 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,20 @@ module Syntropy
1212
class App
1313
attr_reader :route_cache
1414

15-
def initialize(machine, src_path, mount_path, env = {})
15+
def initialize(machine, src_path, mount_path, opts = {})
1616
@machine = machine
17-
@src_path = src_path
17+
@src_path = File.expand_path(src_path)
1818
@mount_path = mount_path
1919
@route_cache = {}
20-
@env = env
20+
@opts = opts
2121

2222
@relative_path_re = calculate_relative_path_re(mount_path)
23-
if (wf = env[:watch_files])
24-
period = wf.is_a?(Numeric) ? wf : 0.1
25-
machine.spin do
26-
Syntropy.file_watch(@machine, src_path, period: period) { invalidate_cache(it) }
27-
rescue Exception => e
28-
p e
29-
p e.backtrace
30-
end
23+
@machine.spin do
24+
# we do startup stuff asynchronously, in order to first let TP2 do its
25+
# setup tasks
26+
@machine.sleep 0.25
27+
@opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
28+
start_file_watcher if opts[:watch_files]
3129
end
3230
end
3331

@@ -42,15 +40,6 @@ def find_route(path, cache: true)
4240
entry
4341
end
4442

45-
def invalidate_cache(fn)
46-
invalidated_keys = []
47-
@route_cache.each do |k, v|
48-
invalidated_keys << k if v[:fn] == fn
49-
end
50-
51-
invalidated_keys.each { @route_cache.delete(it) }
52-
end
53-
5443
def call(req)
5544
entry = find_route(req.path)
5645
render_entry(req, entry)
@@ -62,6 +51,32 @@ def call(req)
6251

6352
private
6453

54+
def start_file_watcher
55+
@opts[:logger]&.call('Watching for module file changes...', nil)
56+
wf = @opts[:watch_files]
57+
period = wf.is_a?(Numeric) ? wf : 0.1
58+
@machine.spin do
59+
Syntropy.file_watch(@machine, @src_path, period: period) do
60+
@opts[:logger]&.call("Detected changed file: #{it}")
61+
invalidate_cache(it)
62+
rescue Exception => e
63+
p e
64+
p e.backtrace
65+
exit!
66+
end
67+
end
68+
end
69+
70+
def invalidate_cache(fn)
71+
invalidated_keys = []
72+
@route_cache.each do |k, v|
73+
@opts[:logger]&.call("Invalidate cache for #{k}", nil)
74+
invalidated_keys << k if v[:fn] == fn
75+
end
76+
77+
invalidated_keys.each { @route_cache.delete(it) }
78+
end
79+
6580
def calculate_relative_path_re(mount_path)
6681
mount_path = '' if mount_path == '/'
6782
/^#{mount_path}(?:\/(.*))?$/
@@ -95,7 +110,7 @@ def calculate_route(path)
95110
end
96111

97112
def file_entry(fn)
98-
{ fn: fn, kind: FILE_KINDS[File.extname(fn)] || :static }
113+
{ fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
99114
end
100115

101116
def find_index_entry(dir)
@@ -138,19 +153,23 @@ def render_entry(req, entry)
138153
when :not_found
139154
req.respond('Not found', ':status' => Qeweney::Status::NOT_FOUND)
140155
when :static
141-
entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
142-
req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
156+
respond_static(req, entry)
143157
when :markdown
144158
body = render_markdown(IO.read(entry[:fn]))
145159
req.respond(body, 'Content-Type' => 'text/html')
146160
when :module
147-
call_module(entry, req)
161+
call_module(req, entry)
148162
else
149-
raise "Invalid entry kind"
163+
raise 'Invalid entry kind'
150164
end
151165
end
152166

153-
def call_module(entry, req)
167+
def respond_static(req, entry)
168+
entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
169+
req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
170+
end
171+
172+
def call_module(req, entry)
154173
entry[:code] ||= load_module(entry)
155174
if entry[:code] == :invalid
156175
req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -165,17 +184,13 @@ def call_module(entry, req)
165184
end
166185

167186
def load_module(entry)
168-
loader = Syntropy::ModuleLoader.new(@src_path, @env)
187+
loader = Syntropy::ModuleLoader.new(@src_path, @opts)
169188
ref = entry[:fn].gsub(%r{^#{@src_path}\/}, '').gsub(/\.rb$/, '')
170189
o = loader.load(ref)
171190
# klass = Class.new
172191
# o = klass.instance_eval(body, entry[:fn], 1)
173192

174-
if o.is_a?(Papercraft::HTML)
175-
return wrap_template(o)
176-
else
177-
return o
178-
end
193+
o.is_a?(Papercraft::HTML) ? wrap_template(o) : o
179194
end
180195

181196
def wrap_template(templ)

syntropy.gemspec

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
2525
s.add_dependency 'json', '2.12.2'
2626
s.add_dependency 'papercraft', '1.4'
2727
s.add_dependency 'qeweney', '0.21'
28-
s.add_dependency 'tp2', '0.12.2'
28+
s.add_dependency 'tp2', '0.12.3.1'
2929
s.add_dependency 'uringmachine', '0.15'
3030

3131
s.add_dependency 'listen', '3.9.0'

0 commit comments

Comments
 (0)