syntropy 0.33.0 → 0.34.1

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 (115) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +11 -0
  3. data/cmd/console.rb +18 -7
  4. data/cmd/serve.rb +26 -18
  5. data/cmd/test.rb +37 -24
  6. data/examples/blog/.gitignore +1 -0
  7. data/examples/blog/app/_layout/default.rb +3 -0
  8. data/examples/blog/app/_lib/database.rb +13 -0
  9. data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
  10. data/examples/blog/app/assets/style.css +20 -0
  11. data/examples/blog/app/index.rb +12 -2
  12. data/examples/blog/app/posts/[id]/edit.rb +2 -2
  13. data/examples/blog/app/posts/[id]/index.rb +4 -4
  14. data/examples/blog/app/posts/index.rb +4 -4
  15. data/examples/blog/app/posts/new.rb +1 -1
  16. data/examples/blog/app/test.rb +7 -0
  17. data/examples/blog/config/development.rb +5 -0
  18. data/examples/blog/config/production.rb +4 -0
  19. data/examples/blog/config/test.rb +5 -0
  20. data/examples/blog/test/test_posts.rb +65 -0
  21. data/examples/mcp-oauth/app/oauth/token.rb +1 -1
  22. data/examples/template/.gitignore +2 -0
  23. data/examples/template/Gemfile +3 -0
  24. data/examples/template/app/_layout/default.rb +14 -0
  25. data/examples/template/app/_lib/database.rb +13 -0
  26. data/examples/template/app/_schema/2026-01-01-initial.rb +9 -0
  27. data/examples/template/app/assets/style.css +25 -0
  28. data/examples/template/app/index.rb +27 -0
  29. data/examples/template/app/test.rb +7 -0
  30. data/examples/template/config/development.rb +5 -0
  31. data/examples/template/config/production.rb +4 -0
  32. data/examples/template/config/test.rb +5 -0
  33. data/examples/template/test/test_app.rb +14 -0
  34. data/lib/syntropy/app.rb +48 -40
  35. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
  36. data/lib/syntropy/applets/builtin/default_error_handler/style.css +4 -8
  37. data/lib/syntropy/applets/builtin/default_error_handler.rb +18 -9
  38. data/lib/syntropy/db/schema.rb +1 -1
  39. data/lib/syntropy/db/store.rb +2 -0
  40. data/lib/syntropy/errors.rb +6 -2
  41. data/lib/syntropy/http/client.rb +1 -0
  42. data/lib/syntropy/http/server_connection.rb +0 -4
  43. data/lib/syntropy/json_api.rb +27 -1
  44. data/lib/syntropy/logger.rb +81 -27
  45. data/lib/syntropy/markdown.rb +61 -32
  46. data/lib/syntropy/mime_types.rb +9 -5
  47. data/lib/syntropy/module_loader.rb +31 -9
  48. data/lib/syntropy/papercraft_extensions.rb +2 -2
  49. data/lib/syntropy/request/mock_adapter.rb +10 -8
  50. data/lib/syntropy/request/request_info.rb +91 -0
  51. data/lib/syntropy/request/response.rb +1 -12
  52. data/lib/syntropy/request/validation.rb +1 -0
  53. data/lib/syntropy/request.rb +51 -19
  54. data/lib/syntropy/routing_tree.rb +27 -28
  55. data/lib/syntropy/session.rb +198 -0
  56. data/lib/syntropy/side_run.rb +25 -2
  57. data/lib/syntropy/test.rb +105 -10
  58. data/lib/syntropy/utils.rb +53 -18
  59. data/lib/syntropy/version.rb +1 -1
  60. data/lib/syntropy.rb +44 -10
  61. data/test/bm_router_proc.rb +4 -4
  62. data/test/fixtures/app/class_instance.rb +5 -0
  63. data/test/fixtures/app/http.rb +5 -0
  64. data/test/fixtures/app/post_ct.rb +5 -0
  65. data/test/fixtures/app/singleton.rb +3 -0
  66. data/test/test_app.rb +13 -52
  67. data/test/test_caching.rb +2 -2
  68. data/test/test_db_schema.rb +1 -1
  69. data/test/test_http_server_connection.rb +3 -3
  70. data/test/test_module_loader.rb +5 -2
  71. data/test/test_response.rb +0 -19
  72. data/test/test_routing_tree.rb +69 -69
  73. data/test/test_server.rb +5 -9
  74. data/test/test_test.rb +70 -0
  75. metadata +66 -42
  76. data/examples/blog/app/_setup.rb +0 -4
  77. data/lib/syntropy/request/session.rb +0 -113
  78. /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
  79. /data/test/{app → fixtures/app}/_hook.rb +0 -0
  80. /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
  81. /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
  82. /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
  83. /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
  84. /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
  85. /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
  86. /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
  87. /data/test/{app → fixtures/app}/about/_error.rb +0 -0
  88. /data/test/{app → fixtures/app}/about/foo.md +0 -0
  89. /data/test/{app → fixtures/app}/about/index.rb +0 -0
  90. /data/test/{app → fixtures/app}/about/raise.rb +0 -0
  91. /data/test/{app → fixtures/app}/api+.rb +0 -0
  92. /data/test/{app → fixtures/app}/assets/style.css +0 -0
  93. /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
  94. /data/test/{app → fixtures/app}/bar.rb +0 -0
  95. /data/test/{app → fixtures/app}/baz.rb +0 -0
  96. /data/test/{app → fixtures/app}/by_method.rb +0 -0
  97. /data/test/{app → fixtures/app}/deps.rb +0 -0
  98. /data/test/{app → fixtures/app}/index.html +0 -0
  99. /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
  100. /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
  101. /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
  102. /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
  103. /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
  104. /data/test/{app → fixtures/app}/rss.rb +0 -0
  105. /data/test/{app → fixtures/app}/tmp.rb +0 -0
  106. /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
  107. /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
  108. /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
  109. /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
  110. /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
  111. /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
  112. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
  113. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
  114. /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
  115. /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
@@ -3,20 +3,26 @@
3
3
  require_relative './request/request_info'
4
4
  require_relative './request/validation'
5
5
  require_relative './request/response'
6
- require_relative './request/session'
6
+ require_relative './session'
7
7
  require_relative './http/status'
8
8
 
9
9
  module Syntropy
10
+ # Syntropy::Request represents an HTTP request. By interacting with the
11
+ # request, the app can extract request information and respond to the request.
10
12
  class Request
11
13
  include RequestInfoMethods
12
14
  include RequestValidationMethods
13
15
  include ResponseMethods
14
-
15
16
  extend RequestInfoClassMethods
16
17
 
17
18
  attr_reader :headers, :adapter, :start_stamp, :route_params
18
19
  attr_accessor :route
19
20
 
21
+ # Initializes the request.
22
+ #
23
+ # @param headers [Hash] request headers
24
+ # @param adapter [Object] connection adapter
25
+ # @return [void]
20
26
  def initialize(headers, adapter)
21
27
  @headers = headers
22
28
  @adapter = adapter
@@ -26,37 +32,62 @@ module Syntropy
26
32
  @ctx = nil
27
33
  end
28
34
 
29
- # Returns the request context
35
+ # Returns the request context, used to store auxiliary information.
36
+ #
37
+ # @return [Hash] request context hash
30
38
  def ctx
31
39
  @ctx ||= {}
32
40
  end
33
41
 
42
+ # Returns the next request body chunk.
43
+ #
44
+ # @return [String, nil]
34
45
  def next_chunk
35
46
  @adapter.get_body_chunk(self)
36
47
  end
37
48
 
49
+ # Reads request body chunks until the entire body is consumed, yielding each
50
+ # chunk to the given block.
51
+ #
52
+ # @return [void]
38
53
  def each_chunk
39
54
  while (chunk = @adapter.get_body_chunk(self))
40
55
  yield chunk
41
56
  end
42
57
  end
43
58
 
59
+ # Reads the request body.
60
+ #
61
+ # @return [String, nil] request body
44
62
  def read
45
63
  @adapter.get_body(self)
46
64
  end
47
65
  alias_method :body, :read
48
66
 
67
+ # Returns true if the request body has been consumed.
68
+ #
69
+ # @return [bool]
49
70
  def complete?
50
71
  @adapter.complete?(self)
51
72
  end
52
73
 
53
74
  EMPTY_HEADERS = {}.freeze
54
75
 
76
+ # Sends a response.
77
+ #
78
+ # @param body [String, nil] response body
79
+ # @param headers [Hash] response headers
80
+ # @return [void]
55
81
  def respond(body, headers = EMPTY_HEADERS)
56
82
  @adapter.respond(self, body, headers)
57
83
  @headers_sent = true
58
84
  end
59
85
 
86
+ # Sends response headers.
87
+ #
88
+ # @param headers [Hash] response headers
89
+ # @param empty_response [bool] body should be sent
90
+ # @return [void]
60
91
  def send_headers(headers = EMPTY_HEADERS, empty_response = false)
61
92
  return if @headers_sent
62
93
 
@@ -64,6 +95,11 @@ module Syntropy
64
95
  @adapter.send_headers(self, headers, empty_response: empty_response)
65
96
  end
66
97
 
98
+ # Sends a response body chunk.
99
+ #
100
+ # @param body [String] response body chunk
101
+ # @param done [bool] body is complete
102
+ # @return [void]
67
103
  def send_chunk(body, done: false)
68
104
  send_headers({}) unless @headers_sent
69
105
 
@@ -71,36 +107,32 @@ module Syntropy
71
107
  end
72
108
  alias_method :<<, :send_chunk
73
109
 
110
+ # Finish response.
111
+ #
112
+ # @return [void]
74
113
  def finish
75
114
  send_headers({}) unless @headers_sent
76
115
 
77
116
  @adapter.finish(self)
78
117
  end
79
118
 
119
+ # Returns true if response headers were sent.
120
+ #
121
+ # @return [bool]
80
122
  def headers_sent?
81
123
  @headers_sent
82
124
  end
83
125
 
84
- def rx_incr(count)
85
- headers[':rx'] ? headers[':rx'] += count : headers[':rx'] = count
86
- end
87
-
88
- def tx_incr(count)
89
- headers[':tx'] ? headers[':tx'] += count : headers[':tx'] = count
90
- end
91
-
92
- def transfer_counts
93
- [headers[':rx'], headers[':tx']]
94
- end
95
-
96
- def total_transfer
97
- (headers[':rx'] || 0) + (headers[':tx'] || 0)
98
- end
99
-
126
+ # Returns the request session.
127
+ #
128
+ # @return [Syntropy::Session]
100
129
  def session
101
130
  @session ||= Session.new(self)
102
131
  end
103
132
 
133
+ # Returns the request flash session storage.
134
+ #
135
+ # @return [Syntropy::Session::Flash]
104
136
  def flash
105
137
  session.flash
106
138
  end
@@ -5,7 +5,7 @@ module Syntropy
5
5
  # static files, markdown files, ruby modules, parametric routes, subtree routes,
6
6
  # nested middleware and error handlers.
7
7
  #
8
- # A RoutingTree instance takes the given directory (root_dir) and constructs a
8
+ # A RoutingTree instance takes the given directory (app_root) and constructs a
9
9
  # tree of route entries corresponding to the directory's contents. Finally, it
10
10
  # generates an optimized router proc, which is used by the application to return
11
11
  # a route entry for each incoming HTTP request.
@@ -41,15 +41,15 @@ module Syntropy
41
41
  # allows you to prevent access through the HTTP server to protected or
42
42
  # internal modules or files.
43
43
  class RoutingTree
44
- attr_reader :root_dir, :mount_path, :static_map, :dynamic_map, :root
44
+ attr_reader :app_root, :mount_path, :static_map, :dynamic_map, :root
45
45
 
46
46
  # Initializes a new RoutingTree instance and computes the routing tree
47
47
  #
48
- # @param root_dir [String] root directory of file tree
48
+ # @param app_root [String] root directory of file tree
49
49
  # @param mount_path [String] base URL path
50
50
  # @return [void]
51
- def initialize(root_dir:, mount_path:, **env)
52
- @root_dir = root_dir
51
+ def initialize(app_root:, mount_path:, **env)
52
+ @app_root = app_root
53
53
  @mount_path = mount_path
54
54
  @static_map = {}
55
55
  @dynamic_map = {}
@@ -72,7 +72,7 @@ module Syntropy
72
72
  # @param fn [String] file path
73
73
  # @return [String] clean path
74
74
  def compute_clean_url_path(fn)
75
- rel_path = fn.sub(@root_dir, '')
75
+ rel_path = fn.sub(@app_root, '')
76
76
  case rel_path
77
77
  when /^(.*)\/index\.(md|rb|html)$/
78
78
  Regexp.last_match(1).then { it == '' ? '/' : it }
@@ -88,7 +88,7 @@ module Syntropy
88
88
  # @param fn [String] filename
89
89
  # @return [String] relative path
90
90
  def fn_to_rel_path(fn)
91
- fn.sub(/^#{Regexp.escape(@root_dir)}\//, '').sub(/\.[^\.]+$/, '')
91
+ fn.sub(/^#{Regexp.escape(@app_root)}\//, '').sub(/\.[^\.]+$/, '')
92
92
  end
93
93
 
94
94
  # Mounts the given applet on the routng tree at the given (absolute) mount
@@ -142,7 +142,7 @@ module Syntropy
142
142
  #
143
143
  # @return [Hash] root entry
144
144
  def compute_tree
145
- compute_route_directory(dir: @root_dir, rel_path: '/', parent: nil)
145
+ compute_route_directory(dir: @app_root, rel_path: '/', parent: nil)
146
146
  end
147
147
 
148
148
  # Converts the given absolute path to a relative one (relative to the
@@ -233,7 +233,7 @@ module Syntropy
233
233
  # @return [String, nil] file path if found
234
234
  def find_aux_module_entry(dir, name)
235
235
  fn = File.join(dir, name)
236
- File.file?(fn) ? ({ kind: :module, fn: }) : nil
236
+ File.file?(fn) ? { kind: :module, fn: } : nil
237
237
  end
238
238
 
239
239
  # Returns a hash mapping file/dir names to route entries.
@@ -247,10 +247,10 @@ module Syntropy
247
247
 
248
248
  rel_path = compute_clean_url_path(fn)
249
249
  child = if File.file?(fn)
250
- compute_route_file(fn:, rel_path:, parent:)
251
- elsif File.directory?(fn)
252
- compute_route_directory(dir: fn, rel_path:, parent:)
253
- end
250
+ compute_route_file(fn:, rel_path:, parent:)
251
+ elsif File.directory?(fn)
252
+ compute_route_directory(dir: fn, rel_path:, parent:)
253
+ end
254
254
  map[child_key(child)] = child if child
255
255
  }
256
256
  end
@@ -317,7 +317,6 @@ module Syntropy
317
317
  set_index_route_target(parent:, path:, kind:, fn:, handle_subtree:)
318
318
  end
319
319
 
320
-
321
320
  # Sets an index route target for the given parent entry. Index files are
322
321
  # applied as targets to the immediate containing directory. HTML index files
323
322
  # are considered static and therefore not added to the routing tree.
@@ -329,7 +328,7 @@ module Syntropy
329
328
  # @param handle_subtree [bool] whether the target handles the subtree
330
329
  # @return [nil] (prevents addition of an index route)
331
330
  def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
332
- if is_parametric_route?(parent) || handle_subtree
331
+ if parametric_route?(parent) || handle_subtree
333
332
  @dynamic_map[path] = parent
334
333
  parent[:target] = { kind:, fn: }
335
334
  parent[:handle_subtree] = handle_subtree
@@ -383,7 +382,7 @@ module Syntropy
383
382
  # @param entry [Hash] route entry
384
383
  def make_route_entry(entry)
385
384
  path = entry[:path]
386
- if is_parametric_route?(entry) || entry[:handle_subtree]
385
+ if parametric_route?(entry) || entry[:handle_subtree]
387
386
  @dynamic_map[path] = entry
388
387
  else
389
388
  entry[:static] = true
@@ -394,8 +393,8 @@ module Syntropy
394
393
  # returns true if the route or any of its ancestors are parametric.
395
394
  #
396
395
  # @param entry [Hash] route entry
397
- def is_parametric_route?(entry)
398
- entry[:param] || (entry[:parent] && is_parametric_route?(entry[:parent]))
396
+ def parametric_route?(entry)
397
+ entry[:param] || (entry[:parent] && parametric_route?(entry[:parent]))
399
398
  end
400
399
 
401
400
  # Converts a relative URL path to absolute URL path.
@@ -468,7 +467,7 @@ module Syntropy
468
467
  emit_router_proc_postlude(buffer, default_route_path: wildcard_root && @root[:path])
469
468
  end
470
469
 
471
- buffer#.tap { puts '*' * 40; puts it; puts }
470
+ buffer # .tap { puts '*' * 40; puts it; puts }
472
471
  end
473
472
 
474
473
  # Emits optimized code for a childless wildcard router.
@@ -540,7 +539,7 @@ module Syntropy
540
539
  return
541
540
  end
542
541
 
543
- if is_void_route?(entry)
542
+ if void_route?(entry)
544
543
  parent = entry[:parent]
545
544
  parametric_sibling = parent && parent[:children] && parent[:children]['[]']
546
545
  if parametric_sibling
@@ -580,7 +579,7 @@ module Syntropy
580
579
  # @param entry [Hash] route entry
581
580
  # @return [Hash, nil] route target if exists
582
581
  def find_target_in_subtree(entry)
583
- entry[:children]&.values&.each { |e|
582
+ entry[:children]&.each_value { |e|
584
583
  target = e[:target] || find_target_in_subtree(e)
585
584
  return target if target
586
585
  }
@@ -593,14 +592,14 @@ module Syntropy
593
592
  #
594
593
  # @param entry [Hash] route entry
595
594
  # @return [bool]
596
- def is_void_route?(entry)
595
+ def void_route?(entry)
597
596
  return false if entry[:param] || entry[:target]
597
+ return true if entry[:static]
598
598
 
599
- if entry[:children]
600
- return true if !entry[:children]['[]'] && entry[:children]&.values&.all? { is_void_route?(it) }
601
- else
602
- return true if entry[:static]
603
- end
599
+ children = entry[:children]
600
+ return false if !children
601
+
602
+ return true if !children['[]'] && children.values.all? { void_route?(it) }
604
603
 
605
604
  false
606
605
  end
@@ -640,7 +639,7 @@ module Syntropy
640
639
 
641
640
  elsif has_children
642
641
  # otherwise look at the next segment
643
- next if is_void_route?(child_entry) && !param_entry
642
+ next if void_route?(child_entry) && !param_entry
644
643
 
645
644
  when_buffer = +''
646
645
  visit_routing_tree_entry(buffer: when_buffer, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'base64'
4
+ require 'json'
5
+ require 'securerandom'
6
+
7
+ module Syntropy
8
+ # A Session object serves as storage for data associated with the user's
9
+ # browser session, such as flash data (for communicating notices and alerts).
10
+ # The session is modeled as a key-value store, where keys are strings, and
11
+ # values can be any value that can be represented in JSON, including arrays
12
+ # and hashes.
13
+ #
14
+ # The session data is stored as an HTTP cookie.
15
+ class Session
16
+ # Initializes the session.
17
+ #
18
+ # @param request [Syntropy::Request] associated request
19
+ # @return [void]
20
+ def initialize(request)
21
+ @request = request
22
+ @data = nil
23
+ end
24
+
25
+ # Returns the value associated with the given key.
26
+ #
27
+ # @param key [String]
28
+ # @return [any] value
29
+ def [](key)
30
+ @data ||= load
31
+ @data[key]
32
+ end
33
+
34
+ # Sets the value for the given key and updates the response session cookie.
35
+ #
36
+ # @param key [String]
37
+ # @param value [any]
38
+ # @return [void]
39
+ def []=(key, value)
40
+ @data ||= load
41
+ @data[key] = value
42
+ save(@data)
43
+ end
44
+
45
+ # Deletes the given key-value pair and updates the response session cookie.
46
+ #
47
+ # @param key [String]
48
+ # @return [any] deleted value
49
+ def delete(key)
50
+ @data ||= load
51
+ value = @data.delete(key)
52
+ save(@data.empty? ? nil : @data)
53
+ value
54
+ end
55
+
56
+ # Discards the session data, updating the response session cookie by
57
+ # emptying it.
58
+ #
59
+ # @return [void]
60
+ def discard
61
+ save(nil)
62
+ end
63
+
64
+ # Returns the flash storage for the session.
65
+ #
66
+ # @return [Syntropy::Session::Flash]
67
+ def flash
68
+ @data ||= load
69
+ @flash ||= Flash.new(self)
70
+ end
71
+
72
+ private
73
+
74
+ # Loads session data from the request session cookie.
75
+ #
76
+ # @return [Hash] session data
77
+ def load
78
+ data = @request.cookies['__syntropy_session__']
79
+ return {} if !data
80
+
81
+ JSON.parse(Base64.decode64(data))
82
+ rescue JSON::ParserError
83
+ {}
84
+ ensure
85
+ @loaded = true
86
+ end
87
+
88
+ # Saves session data to the response session cookie.
89
+ #
90
+ # @param data [Hash] session data
91
+ # @return [void]
92
+ def save(data)
93
+ cookie = data ? "#{Base64.strict_encode64(JSON.dump(data))}; Path=/; HttpOnly" : nil
94
+ @request.set_cookie('__syntropy_session__', cookie)
95
+ end
96
+ end
97
+
98
+ # NowFlash holds flash data for the current request.
99
+ class NowFlash
100
+ def initialize
101
+ @data = {}
102
+ end
103
+
104
+ # Returns the value for the given key.
105
+ #
106
+ # @param key [Symbol]
107
+ # @return [any] flash value
108
+ def [](key)
109
+ @data[key.to_s]
110
+ end
111
+
112
+ # Sets the value for the given key.
113
+ #
114
+ # @param key [Symbol]
115
+ # @param value [any]
116
+ # @return [any] value
117
+ def []=(key, value)
118
+ @data[key.to_s] = value
119
+ end
120
+
121
+ # Iterates through the flash storage, yielding each key-value pair to the
122
+ # given block.
123
+ #
124
+ # @return [void]
125
+ def each(&block)
126
+ @data.each { |k, v| block.(k.to_sym, v) }
127
+ end
128
+ end
129
+
130
+ # Flash acts as a special storage mechanism for transient information that
131
+ # can be passsed between consecutive requests in the same session. Flash
132
+ # values can be set in order to be retrieved in the next response. Reading
133
+ # from flash storage will return data that was set in the previous request.
134
+ # Data written to the flash storage will only be available to the next
135
+ # request. You can also set flash data that will be available to the current
136
+ # request by using Flash#now.
137
+ #
138
+ # In order to keep the read flash data (set in the previous request) and
139
+ # make it available to the next request, use Flash#keep.
140
+ class Flash
141
+ # Initializes the flash storage.
142
+ #
143
+ # @return [void]
144
+ def initialize(session)
145
+ @session = session
146
+ @current_flash_data = @session['_flash']
147
+ @session.delete('_flash') if @current_flash_data
148
+ @current_flash_data ||= {}
149
+ @future_flash_data = {}
150
+ @now_flash_data = NowFlash.new
151
+ end
152
+
153
+ # Reads data from flash storage for the given key. The value would have
154
+ # been set in the previous request.
155
+ #
156
+ # @param key [Symbol]
157
+ # @return [any] value
158
+ def [](key)
159
+ key = key.to_s
160
+ @now_flash_data[key] || @current_flash_data[key]
161
+ end
162
+
163
+ # Sets the flash storage value for the given key. The value would be
164
+ # available to the next request.
165
+ #
166
+ # @param key [Symbol]
167
+ # @param value [any]
168
+ # @return [void]
169
+ def []=(key, value)
170
+ key = key.to_s
171
+ @future_flash_data[key] = value
172
+ @session['_flash'] = @future_flash_data
173
+ end
174
+
175
+ # Iterates through the flash storage, passing each key-value pair to the
176
+ # given block.
177
+ #
178
+ # @return [void]
179
+ def each(&block)
180
+ @current_flash_data.each_pair { |k, v| block.(k.to_sym, v) }
181
+ end
182
+
183
+ # Persists any flash data set in the previous request to the next request.
184
+ #
185
+ # @return [void]
186
+ def keep
187
+ @future_flash_data = @current_flash_data.merge!(@future_flash_data)
188
+ @session['_flash'] = @future_flash_data
189
+ end
190
+
191
+ # Returns the flash storage for the current request.
192
+ #
193
+ # @return [Syntropy::NowFlash]
194
+ def now
195
+ @now_flash_data
196
+ end
197
+ end
198
+ end
@@ -3,8 +3,16 @@
3
3
  require 'etc'
4
4
 
5
5
  module Syntropy
6
+ # SideRun implements running an operation on a separate thread.
6
7
  module SideRun
7
8
  class << self
9
+ # Runs the given block on a separate thread, using UringMachine to wait
10
+ # for the operation to complete. If the operation results in a raised
11
+ # exception, that exception will be reraised in the context of the waiting
12
+ # fiber.
13
+ #
14
+ # @param machine [UringMachine] machine instance
15
+ # @return [any] operation return value
8
16
  def call(machine, &block)
9
17
  setup if !@queue
10
18
 
@@ -15,6 +23,11 @@ module Syntropy
15
23
  result.is_a?(Exception) ? (raise result) : result
16
24
  end
17
25
 
26
+ private
27
+
28
+ # Sets up a thread pool for side-running operations.
29
+ #
30
+ # @return [void]
18
31
  def setup
19
32
  @queue = UM::Queue.new
20
33
  count = (Etc.nprocessors - 1).clamp(2..6)
@@ -23,9 +36,13 @@ module Syntropy
23
36
  }
24
37
  end
25
38
 
39
+ # Runs worker loop for running side-run operations.
40
+ #
41
+ # @param queue [UringMachine::Queue] queue for pulling operations
42
+ # @return [void]
26
43
  def side_run_worker(queue)
27
44
  machine = UM.new
28
- loop { handle_request(machine, queue) }
45
+ loop { run_op(machine, queue) }
29
46
  rescue UM::Terminate
30
47
  # # We can also add a timeout here
31
48
  # t0 = Time.now
@@ -34,7 +51,13 @@ module Syntropy
34
51
  # end
35
52
  end
36
53
 
37
- def handle_request(machine, queue)
54
+ # Pulls an operation from the given queue and runs it, pushing its return
55
+ # value to the corresponding mailbox.
56
+ #
57
+ # @param machine [UringMachine] machine instance
58
+ # @param queue [UringMachine::Queue] op queue
59
+ # @return [void]
60
+ def run_op(machine, queue)
38
61
  response_mailbox, closure = machine.shift(queue)
39
62
  result = closure.call
40
63
  machine.push(response_mailbox, result)