syntropy 0.2 → 0.4

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 (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -4
  3. data/CHANGELOG.md +12 -0
  4. data/README.md +30 -11
  5. data/TODO.md +89 -97
  6. data/bin/syntropy +7 -3
  7. data/cmd/setup/template/site/.gitignore +57 -0
  8. data/cmd/setup/template/site/Dockerfile +32 -0
  9. data/cmd/setup/template/site/Gemfile +3 -0
  10. data/cmd/setup/template/site/README.md +0 -0
  11. data/cmd/setup/template/site/bin/console +0 -0
  12. data/cmd/setup/template/site/bin/restart +0 -0
  13. data/cmd/setup/template/site/bin/server +0 -0
  14. data/cmd/setup/template/site/bin/start +0 -0
  15. data/cmd/setup/template/site/bin/stop +0 -0
  16. data/cmd/setup/template/site/docker-compose.yml +51 -0
  17. data/cmd/setup/template/site/proxy/Dockerfile +5 -0
  18. data/cmd/setup/template/site/proxy/etc/Caddyfile +7 -0
  19. data/cmd/setup/template/site/proxy/etc/tls_auto +2 -0
  20. data/cmd/setup/template/site/proxy/etc/tls_cloudflare +4 -0
  21. data/cmd/setup/template/site/proxy/etc/tls_custom +1 -0
  22. data/cmd/setup/template/site/proxy/etc/tls_selfsigned +1 -0
  23. data/cmd/setup/template/site/site/_layout/default.rb +11 -0
  24. data/cmd/setup/template/site/site/about.md +6 -0
  25. data/cmd/setup/template/site/site/articles/cage.rb +29 -0
  26. data/cmd/setup/template/site/site/articles/index.rb +3 -0
  27. data/cmd/setup/template/site/site/assets/css/style.css +40 -0
  28. data/cmd/setup/template/site/site/assets/img/syntropy.png +0 -0
  29. data/cmd/setup/template/site/site/index.rb +15 -0
  30. data/docker-compose.yml +51 -0
  31. data/lib/syntropy/app.rb +134 -50
  32. data/lib/syntropy/errors.rb +16 -2
  33. data/lib/syntropy/module.rb +26 -5
  34. data/lib/syntropy/request_extensions.rb +96 -0
  35. data/lib/syntropy/rpc_api.rb +26 -9
  36. data/lib/syntropy/side_run.rb +46 -0
  37. data/lib/syntropy/version.rb +1 -1
  38. data/lib/syntropy.rb +14 -49
  39. data/syntropy.gemspec +1 -1
  40. data/test/app/baz.rb +3 -0
  41. data/test/test_app.rb +57 -7
  42. data/test/test_side_run.rb +43 -0
  43. data/test/test_validation.rb +1 -1
  44. metadata +31 -3
@@ -0,0 +1,51 @@
1
+ services:
2
+ backend:
3
+ build: .
4
+ privileged: true
5
+ restart: always
6
+ ports:
7
+ - 127.0.0.1:1234:1234
8
+ # expose:
9
+ # - 1234
10
+ volumes:
11
+ - .:/home/app
12
+ deploy:
13
+ # replicas: 1
14
+ resources:
15
+ limits:
16
+ memory: 500M
17
+ # restart: unless-stopped
18
+ logging:
19
+ driver: "json-file"
20
+ options:
21
+ max-size: "1M"
22
+ max-file: "10"
23
+
24
+ # healthcheck:
25
+ # test: "curl 'http://localhost:1234/?q=ping'"
26
+ # interval: "30s"
27
+ # timeout: "3s"
28
+ # start_period: "5s"
29
+ # retries: 3
30
+
31
+ proxy:
32
+ depends_on:
33
+ - backend
34
+ build:
35
+ context: ./proxy
36
+ dockerfile: Dockerfile
37
+ restart: always
38
+ volumes:
39
+ - ./proxy/etc/Caddyfile:/etc/caddy/Caddyfile
40
+ ports:
41
+ - "80:80"
42
+ - "443:443"
43
+ - "443:443/udp"
44
+ # env_file:
45
+ # - ./conf/caddy.env
46
+ # - ./conf/caddy_sensitive.env
47
+ logging:
48
+ driver: "json-file"
49
+ options:
50
+ max-size: "1M"
51
+ max-file: "10"
data/lib/syntropy/app.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'qeweney'
4
3
  require 'json'
4
+ require 'yaml'
5
+
6
+ require 'qeweney'
5
7
  require 'papercraft'
6
8
 
7
9
  require 'syntropy/errors'
@@ -12,23 +14,23 @@ module Syntropy
12
14
  class App
13
15
  attr_reader :route_cache
14
16
 
15
- def initialize(machine, src_path, mount_path, env = {})
17
+ def initialize(machine, src_path, mount_path, opts = {})
16
18
  @machine = machine
17
- @src_path = src_path
19
+ @src_path = File.expand_path(src_path)
18
20
  @mount_path = mount_path
19
21
  @route_cache = {}
20
- @env = env
22
+ @opts = opts
21
23
 
22
24
  @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
25
+ @machine.spin do
26
+ # we do startup stuff asynchronously, in order to first let TP2 do its
27
+ # setup tasks
28
+ @machine.sleep 0.15
29
+ @opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
30
+ start_file_watcher if opts[:watch_files]
31
31
  end
32
+
33
+ @module_loader ||= Syntropy::ModuleLoader.new(@src_path, @opts)
32
34
  end
33
35
 
34
36
  def find_route(path, cache: true)
@@ -36,24 +38,16 @@ module Syntropy
36
38
  return cached if cached
37
39
 
38
40
  entry = calculate_route(path)
39
- if entry[:kind] != :not_found
40
- @route_cache[path] = entry if cache
41
- end
41
+ @route_cache[path] = entry if entry[:kind] != :not_found && cache
42
42
  entry
43
43
  end
44
44
 
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
-
54
45
  def call(req)
55
46
  entry = find_route(req.path)
56
47
  render_entry(req, entry)
48
+ rescue Syntropy::Error => e
49
+ msg = e.message
50
+ req.respond(msg.empty? ? nil : msg, ':status' => e.http_status)
57
51
  rescue StandardError => e
58
52
  p e
59
53
  p e.backtrace
@@ -62,9 +56,37 @@ module Syntropy
62
56
 
63
57
  private
64
58
 
59
+ def start_file_watcher
60
+ @opts[:logger]&.call('Watching for module file changes...', nil)
61
+ wf = @opts[:watch_files]
62
+ period = wf.is_a?(Numeric) ? wf : 0.1
63
+ @machine.spin do
64
+ Syntropy.file_watch(@machine, @src_path, period: period) do
65
+ @opts[:logger]&.call("Detected changed file: #{it}")
66
+ invalidate_cache(it)
67
+ rescue Exception => e
68
+ p e
69
+ p e.backtrace
70
+ exit!
71
+ end
72
+ end
73
+ end
74
+
75
+ def invalidate_cache(fn)
76
+ @module_loader.unload(fn)
77
+
78
+ invalidated_keys = []
79
+ @route_cache.each do |k, v|
80
+ @opts[:logger]&.call("Invalidate cache for #{k}", nil)
81
+ invalidated_keys << k if v[:fn] == fn
82
+ end
83
+
84
+ invalidated_keys.each { @route_cache.delete(it) }
85
+ end
86
+
65
87
  def calculate_relative_path_re(mount_path)
66
88
  mount_path = '' if mount_path == '/'
67
- /^#{mount_path}(?:\/(.*))?$/
89
+ %r{^#{mount_path}(?:/(.*))?$}
68
90
  end
69
91
 
70
92
  FILE_KINDS = {
@@ -74,7 +96,7 @@ module Syntropy
74
96
  NOT_FOUND = { kind: :not_found }
75
97
 
76
98
  # We don't allow access to path with /.., or entries that start with _
77
- FORBIDDEN_RE = /(\/_)|((\/\.\.)\/?)/
99
+ FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
78
100
 
79
101
  def calculate_route(path)
80
102
  return NOT_FOUND if path =~ FORBIDDEN_RE
@@ -95,7 +117,7 @@ module Syntropy
95
117
  end
96
118
 
97
119
  def file_entry(fn)
98
- { fn: fn, kind: FILE_KINDS[File.extname(fn)] || :static }
120
+ { fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
99
121
  end
100
122
 
101
123
  def find_index_entry(dir)
@@ -126,7 +148,7 @@ module Syntropy
126
148
  entry[:kind] == :module ? entry : NOT_FOUND
127
149
  end
128
150
 
129
- UP_TREE_PATH_RE = /^(.+)?\/[^\/]+$/
151
+ UP_TREE_PATH_RE = %r{^(.+)?/[^/]+$}
130
152
 
131
153
  def parent_path(path)
132
154
  m = path.match(UP_TREE_PATH_RE)
@@ -136,21 +158,47 @@ module Syntropy
136
158
  def render_entry(req, entry)
137
159
  case entry[:kind]
138
160
  when :not_found
139
- req.respond('Not found', ':status' => Qeweney::Status::NOT_FOUND)
161
+ respond_not_found(req, entry)
140
162
  when :static
141
- entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
142
- req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
163
+ respond_static(req, entry)
143
164
  when :markdown
144
- body = render_markdown(IO.read(entry[:fn]))
145
- req.respond(body, 'Content-Type' => 'text/html')
165
+ respond_markdown(req, entry)
146
166
  when :module
147
- call_module(entry, req)
167
+ respond_module(req, entry)
148
168
  else
149
- raise "Invalid entry kind"
169
+ raise 'Invalid entry kind'
150
170
  end
151
171
  end
152
172
 
153
- def call_module(entry, req)
173
+ def respond_not_found(req, _entry)
174
+ headers = { ':status' => Qeweney::Status::NOT_FOUND }
175
+ case req.method
176
+ when 'head'
177
+ req.respond(nil, headers)
178
+ else
179
+ req.respond('Not found', headers)
180
+ end
181
+ end
182
+
183
+ def respond_static(req, entry)
184
+ entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
185
+ headers = { 'Content-Type' => entry[:mime_type] }
186
+ req.respond_by_http_method(
187
+ 'head' => [nil, headers],
188
+ 'get' => -> { [IO.read(entry[:fn]), headers] }
189
+ )
190
+ end
191
+
192
+ def respond_markdown(req, entry)
193
+ entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
194
+ headers = { 'Content-Type' => entry[:mime_type] }
195
+ req.respond_by_http_method(
196
+ 'head' => [nil, headers],
197
+ 'get' => -> { [render_markdown(entry[:fn]), headers] }
198
+ )
199
+ end
200
+
201
+ def respond_module(req, entry)
154
202
  entry[:code] ||= load_module(entry)
155
203
  if entry[:code] == :invalid
156
204
  req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
@@ -158,6 +206,8 @@ module Syntropy
158
206
  end
159
207
 
160
208
  entry[:code].call(req)
209
+ rescue Syntropy::Error => e
210
+ req.respond(nil, ':status' => e.http_status)
161
211
  rescue StandardError => e
162
212
  p e
163
213
  p e.backtrace
@@ -165,28 +215,62 @@ module Syntropy
165
215
  end
166
216
 
167
217
  def load_module(entry)
168
- loader = Syntropy::ModuleLoader.new(@src_path, @env)
169
- ref = entry[:fn].gsub(%r{^#{@src_path}\/}, '').gsub(/\.rb$/, '')
170
- o = loader.load(ref)
171
- # klass = Class.new
172
- # o = klass.instance_eval(body, entry[:fn], 1)
173
-
174
- if o.is_a?(Papercraft::HTML)
175
- return wrap_template(o)
176
- else
177
- return o
178
- end
218
+ ref = entry[:fn].gsub(%r{^#{@src_path}/}, '').gsub(/\.rb$/, '')
219
+ o = @module_loader.load(ref)
220
+ o.is_a?(Papercraft::Template) ? wrap_template(o) : o
221
+ rescue Exception => e
222
+ @opts[:logger]&.call("Error while loading module #{ref}: #{e.message}")
223
+ :invalid
179
224
  end
180
225
 
181
226
  def wrap_template(templ)
182
- ->(req) {
227
+ lambda { |req|
183
228
  body = templ.render
184
229
  req.respond(body, 'Content-Type' => 'text/html')
185
230
  }
186
231
  end
187
232
 
188
- def render_markdown(str)
189
- Papercraft.markdown(str)
233
+ def render_markdown(fn)
234
+ atts, md = parse_markdown_file(fn)
235
+
236
+ if atts[:layout]
237
+ layout = @module_loader.load("_layout/#{atts[:layout]}")
238
+ html = layout.apply { emit_markdown(md) }.render
239
+ else
240
+ html = Papercraft.markdown(md)
241
+ end
242
+ html
243
+ end
244
+
245
+ DATE_REGEXP = /(\d{4}\-\d{2}\-\d{2})/
246
+ FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
247
+ YAML_OPTS = {
248
+ permitted_classes: [Date],
249
+ symbolize_names: true
250
+ }
251
+
252
+ # Parses the markdown file at the given path.
253
+ #
254
+ # @param path [String] file path
255
+ # @return [Array] an tuple containing properties<Hash>, contents<String>
256
+ def parse_markdown_file(path)
257
+ content = IO.read(path) || ''
258
+ atts = {}
259
+
260
+ # Parse date from file name
261
+ if (m = path.match(DATE_REGEXP))
262
+ atts[:date] ||= Date.parse(m[1])
263
+ end
264
+
265
+ if (m = content.match(FRONT_MATTER_REGEXP))
266
+ front_matter = m[1]
267
+ content = m.post_match
268
+
269
+ yaml = YAML.safe_load(front_matter, **YAML_OPTS)
270
+ atts = atts.merge(yaml)
271
+ end
272
+
273
+ [atts, content]
190
274
  end
191
275
  end
192
276
  end
@@ -4,17 +4,31 @@ require 'qeweney'
4
4
 
5
5
  module Syntropy
6
6
  class Error < StandardError
7
+ Status = Qeweney::Status
8
+
7
9
  attr_reader :http_status
8
10
 
9
11
  def initialize(status, msg = '')
10
- @http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
11
12
  super(msg)
13
+ @http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
14
+ end
15
+
16
+ class << self
17
+ # Create class methods for common errors
18
+ {
19
+ not_found: Status::NOT_FOUND,
20
+ method_not_allowed: Status::METHOD_NOT_ALLOWED,
21
+ teapot: Status::TEAPOT
22
+ }
23
+ .each { |k, v|
24
+ define_method(k) { |msg = ''| new(v, msg) }
25
+ }
12
26
  end
13
27
  end
14
28
 
15
29
  class ValidationError < Error
16
30
  def initialize(msg)
17
- @http_status = Qeweney::Status::BAD_REQUEST
31
+ super(Qeweney::Status::BAD_REQUEST, msg)
18
32
  end
19
33
  end
20
34
  end
@@ -1,37 +1,54 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'papercraft'
4
+
3
5
  module Syntropy
4
6
  class ModuleLoader
5
7
  def initialize(root, env)
6
8
  @root = root
7
9
  @env = env
8
- @loaded = {}
10
+ @loaded = {} # maps ref to code
11
+ @fn_map = {} # maps filename to ref
9
12
  end
10
13
 
11
14
  def load(ref)
12
15
  @loaded[ref] ||= load_module(ref)
13
16
  end
14
17
 
18
+ def unload(fn)
19
+ ref = @fn_map[fn]
20
+ return if !ref
21
+
22
+ @loaded.delete(ref)
23
+ @fn_map.delete(fn)
24
+ end
25
+
15
26
  private
16
27
 
17
28
  def load_module(ref)
18
- fn = File.join(@root, "#{ref}.rb")
19
- raise RuntimeError, "File not found #{fn}" if !File.file?(fn)
29
+ fn = File.expand_path(File.join(@root, "#{ref}.rb"))
30
+ @fn_map[fn] = ref
31
+ raise "File not found #{fn}" if !File.file?(fn)
20
32
 
21
33
  mod_body = IO.read(fn)
22
34
  mod_ctx = Class.new(Syntropy::Module)
23
35
  mod_ctx.loader = self
24
- # mod_ctx = .new(self, @env)
25
36
  mod_ctx.module_eval(mod_body, fn, 1)
26
37
 
27
38
  export_value = mod_ctx.__export_value__
28
39
 
40
+ wrap_module(mod_ctx, export_value)
41
+ end
42
+
43
+ def wrap_module(mod_ctx, export_value)
29
44
  case export_value
30
45
  when nil
31
- raise RuntimeError, 'No export found'
46
+ raise 'No export found'
32
47
  when Symbol
33
48
  # TODO: verify export_value denotes a valid method
34
49
  mod_ctx.new(@env)
50
+ when String
51
+ ->(req) { req.respond(export_value) }
35
52
  when Proc
36
53
  export_value
37
54
  else
@@ -57,6 +74,10 @@ module Syntropy
57
74
  @__export_value__ = ref
58
75
  end
59
76
 
77
+ def self.templ(&block)
78
+ Papercraft.html(&block)
79
+ end
80
+
60
81
  def self.__export_value__
61
82
  @__export_value__
62
83
  end
@@ -0,0 +1,96 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'qeweney'
4
+
5
+ module Syntropy
6
+ module RequestExtensions
7
+ def ctx
8
+ @ctx ||= {}
9
+ end
10
+
11
+ def validate_http_method(*accepted)
12
+ raise Syntropy::Error.method_not_allowed if !accepted.include?(method)
13
+ end
14
+
15
+ def respond_by_http_method(map)
16
+ value = map[self.method]
17
+ raise Syntropy::Error.method_not_allowed if !value
18
+
19
+ value = value.() if value.is_a?(Proc)
20
+ (body, headers) = value
21
+ respond(body, headers)
22
+ end
23
+
24
+ def respond_on_get(body, headers = {})
25
+ case self.method
26
+ when 'head'
27
+ respond(nil, headers)
28
+ when 'get'
29
+ respond(body, headers)
30
+ else
31
+ raise Syntropy::Error.method_not_allowed
32
+ end
33
+ end
34
+
35
+ def respond_on_post(body, headers = {})
36
+ case self.method
37
+ when 'head'
38
+ respond(nil, headers)
39
+ when 'post'
40
+ respond(body, headers)
41
+ else
42
+ raise Syntropy::Error.method_not_allowed
43
+ end
44
+ end
45
+
46
+ def validate_param(name, *clauses)
47
+ value = query[name]
48
+ clauses.each do |c|
49
+ valid = param_is_valid?(value, c)
50
+ raise(Syntropy::ValidationError, 'Validation error') if !valid
51
+
52
+ value = param_convert(value, c)
53
+ end
54
+ value
55
+ end
56
+
57
+ private
58
+
59
+ BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
60
+ BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
61
+ INTEGER_REGEXP = /^[+-]?[0-9]+$/
62
+ FLOAT_REGEXP = /^[+-]?[0-9]+(\.[0-9]+)?$/
63
+
64
+ def param_is_valid?(value, cond)
65
+ return cond.any? { |c| param_is_valid?(value, c) } if cond.is_a?(Array)
66
+
67
+ if value
68
+ if cond == :bool
69
+ return value =~ BOOL_REGEXP
70
+ elsif cond == Integer
71
+ return value =~ INTEGER_REGEXP
72
+ elsif cond == Float
73
+ return value =~ FLOAT_REGEXP
74
+ end
75
+ end
76
+
77
+ cond === value
78
+ end
79
+
80
+ def param_convert(value, klass)
81
+ if klass == :bool
82
+ value =~ BOOL_TRUE_REGEXP ? true : false
83
+ elsif klass == Integer
84
+ value.to_i
85
+ elsif klass == Float
86
+ value.to_f
87
+ elsif klass == Symbol
88
+ value.to_sym
89
+ else
90
+ value
91
+ end
92
+ end
93
+ end
94
+ end
95
+
96
+ Qeweney::Request.include(Syntropy::RequestExtensions)
@@ -11,7 +11,7 @@ module Syntropy
11
11
  end
12
12
 
13
13
  def call(req)
14
- response, status = invoke(req)
14
+ response, status = __invoke__(req)
15
15
  req.respond(
16
16
  response.to_json,
17
17
  ':status' => status,
@@ -19,15 +19,17 @@ module Syntropy
19
19
  )
20
20
  end
21
21
 
22
- def invoke(req)
23
- q = req.validate_param(:q, String)
22
+ private
23
+
24
+ def __invoke__(req)
25
+ q = req.validate_param(:q, String).to_sym
24
26
  response = case req.method
25
27
  when 'get'
26
- send(q.to_sym, req)
28
+ __invoke_get__(q, req)
27
29
  when 'post'
28
- send(:"#{q}!", req)
30
+ __invoke_post__(q, req)
29
31
  else
30
- raise Syntropy::Error.new(Qeweney::Status::METHOD_NOT_ALLOWED)
32
+ raise Syntropy::Error.method_not_allowed
31
33
  end
32
34
  [{ status: 'OK', response: response }, Qeweney::Status::OK]
33
35
  rescue => e
@@ -35,14 +37,29 @@ module Syntropy
35
37
  p e
36
38
  p e.backtrace
37
39
  end
38
- error_response(e)
40
+ __error_response__(e)
41
+ end
42
+
43
+ def __invoke_get__(sym, req)
44
+ return send(sym, req) if respond_to?(sym)
45
+
46
+ err = respond_to?(:"#{sym}!") ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
47
+ raise err
48
+ end
49
+
50
+ def __invoke_post__(sym, req)
51
+ sym_post = :"#{sym}!"
52
+ return send(sym_post, req) if respond_to?(sym_post)
53
+
54
+ err = respond_to?(sym) ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
55
+ raise err
39
56
  end
40
57
 
41
58
  INTERNAL_SERVER_ERROR = Qeweney::Status::INTERNAL_SERVER_ERROR
42
59
 
43
- def error_response(err)
60
+ def __error_response__(err)
44
61
  http_status = err.respond_to?(:http_status) ? err.http_status : INTERNAL_SERVER_ERROR
45
- error_name = err.class.name
62
+ error_name = err.class.name.split('::').last
46
63
  [{ status: error_name, message: err.message }, http_status]
47
64
  end
48
65
  end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module Syntropy
6
+ module SideRun
7
+ class << self
8
+ def call(machine, &block)
9
+ setup if !@queue
10
+
11
+ # TODO: share mailboxes, acquire them with e.g. with_mailbox { |mbox| ... }
12
+ mailbox = Thread.current[:fiber_mailbox] ||= UM::Queue.new
13
+ machine.push(@queue, [mailbox, block])
14
+ result = machine.shift(mailbox)
15
+ result.is_a?(Exception) ? (raise result) : result
16
+ end
17
+
18
+ def setup
19
+ @queue = UM::Queue.new
20
+ count = (Etc.nprocessors - 1).clamp(2..6)
21
+ @workers = count.times.map {
22
+ Thread.new { side_run_worker(@queue) }
23
+ }
24
+ end
25
+
26
+ def side_run_worker(queue)
27
+ machine = UM.new
28
+ loop { handle_request(machine, queue) }
29
+ rescue UM::Terminate
30
+ # # We can also add a timeout here
31
+ # t0 = Time.now
32
+ # while !queue.empty? && (Time.now - t0) < 10
33
+ # handle_request(machine, queue)
34
+ # end
35
+ end
36
+
37
+ def handle_request(machine, queue)
38
+ response_mailbox, closure = machine.shift(queue)
39
+ result = closure.call
40
+ machine.push(response_mailbox, result)
41
+ rescue Exception => e
42
+ machine.push(response_mailbox, e)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.2'
4
+ VERSION = '0.4'
5
5
  end