syntropy 0.30.0 → 0.32.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +30 -0
  4. data/TODO.md +46 -1
  5. data/bin/syntropy +8 -86
  6. data/cmd/_banner.rb +16 -0
  7. data/cmd/console.rb +77 -0
  8. data/cmd/help.rb +12 -0
  9. data/cmd/serve.rb +95 -0
  10. data/cmd/test.rb +40 -0
  11. data/examples/{counter.rb → basic/counter.rb} +1 -1
  12. data/examples/{templates.rb → basic/templates.rb} +1 -1
  13. data/examples/blog/app/_layout/default.rb +11 -0
  14. data/examples/blog/app/_lib/post_store.rb +47 -0
  15. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  16. data/examples/blog/app/_setup.rb +4 -0
  17. data/examples/blog/app/index.rb +7 -0
  18. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  19. data/examples/blog/app/posts/[id]/index.rb +58 -0
  20. data/examples/blog/app/posts/index.rb +38 -0
  21. data/examples/blog/app/posts/new.rb +29 -0
  22. data/examples/mcp-oauth/.ruby-version +1 -0
  23. data/examples/mcp-oauth/Gemfile +8 -0
  24. data/examples/mcp-oauth/README.md +128 -0
  25. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  26. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  27. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  28. data/examples/mcp-oauth/app/index.md +1 -0
  29. data/examples/mcp-oauth/app/mcp.rb +85 -0
  30. data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
  31. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  32. data/examples/mcp-oauth/app/oauth/register.rb +14 -0
  33. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  34. data/examples/mcp-oauth/app/signin.rb +85 -0
  35. data/examples/mcp-oauth/test/helper.rb +9 -0
  36. data/examples/mcp-oauth/test/test_app.rb +27 -0
  37. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  38. data/lib/syntropy/app.rb +34 -9
  39. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  40. data/lib/syntropy/applets/builtin/req.rb +1 -1
  41. data/lib/syntropy/db/connection_pool.rb +71 -0
  42. data/lib/syntropy/db/schema.rb +92 -0
  43. data/lib/syntropy/db/store.rb +31 -0
  44. data/lib/syntropy/dev_mode.rb +1 -1
  45. data/lib/syntropy/errors.rb +6 -0
  46. data/lib/syntropy/http/client.rb +43 -0
  47. data/lib/syntropy/http/client_connection.rb +36 -0
  48. data/lib/syntropy/http/io_extensions.rb +176 -0
  49. data/lib/syntropy/http/server.rb +5 -5
  50. data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
  51. data/lib/syntropy/http.rb +3 -1
  52. data/lib/syntropy/logger.rb +5 -1
  53. data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
  54. data/lib/syntropy/papercraft_extensions.rb +1 -1
  55. data/lib/syntropy/request/mock_adapter.rb +2 -0
  56. data/lib/syntropy/request/request_info.rb +22 -4
  57. data/lib/syntropy/request/response.rb +2 -2
  58. data/lib/syntropy/request/validation.rb +11 -5
  59. data/lib/syntropy/routing_tree.rb +2 -1
  60. data/lib/syntropy/test.rb +77 -0
  61. data/lib/syntropy/version.rb +1 -1
  62. data/lib/syntropy.rb +5 -23
  63. data/syntropy.gemspec +3 -3
  64. data/test/app/.well-known/foo.rb +3 -0
  65. data/test/app/_hook.rb +1 -1
  66. data/test/app/by_method.rb +9 -0
  67. data/test/app_setup/_setup.rb +7 -0
  68. data/test/app_setup/index.rb +1 -0
  69. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  70. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  71. data/test/helper.rb +1 -25
  72. data/test/schema/2026-01-02-foo.rb +12 -0
  73. data/test/schema/2026-05-30-bar.rb +7 -0
  74. data/test/test_app.rb +110 -70
  75. data/test/test_caching.rb +1 -1
  76. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  77. data/test/test_db_schema.rb +96 -0
  78. data/test/test_db_store.rb +24 -0
  79. data/test/test_http_client.rb +52 -0
  80. data/test/test_http_client_connection.rb +43 -0
  81. data/test/test_http_protocol.rb +250 -0
  82. data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
  83. data/test/test_json_api.rb +5 -5
  84. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  85. data/test/{test_request_extensions.rb → test_request.rb} +153 -18
  86. data/test/test_routing_tree.rb +15 -3
  87. data/test/test_server.rb +9 -13
  88. metadata +84 -36
  89. data/lib/syntropy/connection_pool.rb +0 -61
  90. data/test/test_request_info.rb +0 -90
  91. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  92. /data/examples/{card.rb → basic/card.rb} +0 -0
  93. /data/examples/{counter.js → basic/counter.js} +0 -0
  94. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  95. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  96. /data/examples/{index.md → basic/index.md} +0 -0
data/lib/syntropy/app.rb CHANGED
@@ -6,7 +6,7 @@ require 'yaml'
6
6
  require 'papercraft'
7
7
 
8
8
  require 'syntropy/errors'
9
- require 'syntropy/module'
9
+ require 'syntropy/module_loader'
10
10
  require 'syntropy/routing_tree'
11
11
  require 'syntropy/mime_types'
12
12
 
@@ -35,6 +35,7 @@ module Syntropy
35
35
  end
36
36
 
37
37
  attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :env
38
+ attr_accessor :raise_on_internal_server_error
38
39
 
39
40
  def initialize(**env)
40
41
  @machine = env[:machine]
@@ -101,6 +102,21 @@ module Syntropy
101
102
  route
102
103
  end
103
104
 
105
+ def setup_db(db_path:, schema_root: '_schema')
106
+ @env[:db_path] = db_path
107
+ @env[:schema_root] = schema_root
108
+
109
+ class << self
110
+ def connection_pool
111
+ @connection_pool ||= DB::ConnectionPool.new(@machine, @env[:db_path], 4)
112
+ end
113
+
114
+ def schema
115
+ @schema ||= DB::Schema.new(module_loader: @module_loader, schema_root: @env[:schema_root])
116
+ end
117
+ end
118
+ end
119
+
104
120
  private
105
121
 
106
122
  # Handles a not found error, taking into account hooks up the tree from the
@@ -321,7 +337,7 @@ module Syntropy
321
337
  }
322
338
  body {
323
339
  markdown md
324
- auto_refresh_watch! if @env[:dev_mode]
340
+ auto_refresh! if @env[:dev_mode]
325
341
  }
326
342
  }
327
343
  }
@@ -450,16 +466,28 @@ module Syntropy
450
466
  end
451
467
 
452
468
  RAW_DEFAULT_ERROR_HANDLER = ->(req, err) {
469
+ status = Syntropy::Error.http_status(err)
470
+
453
471
  msg = err.message
454
472
  msg = nil if msg.empty? || (req.method == 'head')
455
- req.respond(msg, ':status' => Syntropy::Error.http_status(err)) rescue nil
473
+ req.respond(msg, ':status' => status) rescue nil
456
474
  }
457
475
 
458
- def default_error_handler
476
+ RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER = ->(req, err) {
477
+ status = Syntropy::Error.http_status(err)
478
+ raise if status == HTTP::INTERNAL_SERVER_ERROR
459
479
 
480
+ msg = err.message
481
+ msg = nil if msg.empty? || (req.method == 'head')
482
+ req.respond(msg, ':status' => status) rescue nil
483
+ }
484
+
485
+ def default_error_handler
460
486
  @default_error_handler ||= begin
461
487
  if @builtin_applet
462
488
  @builtin_applet.module_loader.load('/default_error_handler')
489
+ elsif @raise_on_internal_server_error
490
+ RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER
463
491
  else
464
492
  RAW_DEFAULT_ERROR_HANDLER
465
493
  end
@@ -471,6 +499,8 @@ module Syntropy
471
499
  #
472
500
  # @return [void]
473
501
  def start
502
+ @module_loader.load('_setup', raise_on_missing: false)
503
+
474
504
  @machine.spin do
475
505
  # we do startup stuff asynchronously, in order to first let Syntropy do
476
506
  # its setup tasks.
@@ -489,9 +519,6 @@ module Syntropy
489
519
  #
490
520
  # @return [void]
491
521
  def file_watcher_loop
492
- wf = @env[:watch_files]
493
- period = wf.is_a?(Numeric) ? wf : 0.1
494
-
495
522
  @machine.file_watch(@root_dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
496
523
  fn = e[:fn]
497
524
  @logger&.info(message: 'File change detected', fn: fn)
@@ -499,8 +526,6 @@ module Syntropy
499
526
  debounce_file_change
500
527
  }
501
528
 
502
-
503
-
504
529
  # Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
505
530
  # @logger&.info(message: 'File change detected', fn: fn)
506
531
  # @module_loader.invalidate_fn(fn)
@@ -23,7 +23,7 @@ ErrorPage = ->(error:, status:, backtrace:) {
23
23
  }
24
24
  end
25
25
  }
26
- auto_refresh_watch!
26
+ auto_refresh!
27
27
  }
28
28
  }
29
29
  }
@@ -32,7 +32,7 @@ def transform_backtrace(backtrace)
32
32
  backtrace.map do
33
33
  if (m = it.match(/^(.+:\d+):/))
34
34
  location = m[1]
35
- { entry: it, url: "vscode://file/#{location}" }
35
+ { entry: it, url: "zed://file/#{location}" }
36
36
  else
37
37
  { entry: it, url: nil }
38
38
  end
@@ -43,7 +43,7 @@ def error_response_html(req, error)
43
43
  status = Syntropy::Error.http_status(error)
44
44
  backtrace = transform_backtrace(error.backtrace)
45
45
  html = Papercraft.html(ErrorPage, error:, status:, backtrace:)
46
- req.html_response(html, ':status' => status)
46
+ req.respond_html(html, ':status' => status)
47
47
  end
48
48
 
49
49
  def error_response_raw(req, error)
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  export ->(req) {
4
- req.json_response({
4
+ req.respond_json({
5
5
  headers: req.headers
6
6
  })
7
7
  }
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extralite'
4
+
5
+ module Syntropy
6
+ module DB
7
+ class ConnectionPool
8
+ attr_reader :count
9
+
10
+ def initialize(machine, fn, max_conn)
11
+ @machine = machine
12
+ @fn = fn
13
+ @count = 0
14
+ @max_conn = max_conn
15
+ @queue = UM::Queue.new
16
+ @key = :"connection_pool_#{object_id}"
17
+ end
18
+
19
+ def with_db
20
+ if (db = Thread.current[@key])
21
+ @machine.snooze
22
+ return yield(db)
23
+ end
24
+
25
+ db = checkout
26
+ begin
27
+ Thread.current[@key] = db
28
+ yield(db)
29
+ ensure
30
+ Thread.current[@key] = nil
31
+ checkin(db)
32
+ end
33
+ end
34
+
35
+ def query(sql, *, **, &)
36
+ with_db { it.query(sql, *, **, &) }
37
+ end
38
+
39
+ def execute(sql, *, **)
40
+ with_db { it.execute(sql, *, **) }
41
+ end
42
+
43
+ def close
44
+ while @queue.count > 0
45
+ db = @machine.shift(@queue)
46
+ db.close
47
+ @count -= 1
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def checkout
54
+ return make_db_instance if @queue.count == 0 && @count < @max_conn
55
+
56
+ @machine.shift(@queue)
57
+ end
58
+
59
+ def checkin(db)
60
+ @machine.push(@queue, db)
61
+ end
62
+
63
+ def make_db_instance
64
+ Extralite::Database.new(@fn, wal: true).tap do
65
+ @count += 1
66
+ it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ module DB
5
+ class Schema
6
+ def initialize(module_loader: nil, schema_root: '_schema', &)
7
+ @migrations = {}
8
+ @module_loader = module_loader
9
+ @schema_root = schema_root
10
+ load_schema_from_modules if @module_loader
11
+ run_schema_block(&) if block_given?
12
+ end
13
+
14
+ def apply(connection_pool)
15
+ execute_migrations(connection_pool)
16
+ end
17
+
18
+ def current_version(connection_pool)
19
+ connection_pool.with_db do |db|
20
+ get_schema_version(db)
21
+ end
22
+ end
23
+
24
+ private
25
+
26
+ def load_schema_from_modules
27
+ modules = @module_loader.list(@schema_root)
28
+ modules.each do |name|
29
+ @migrations[File.basename(name)] = @module_loader.load(name)
30
+ end
31
+ end
32
+
33
+ class SchemaBlockRunner
34
+ def initialize(migrations, &)
35
+ @migrations = migrations
36
+ instance_eval(&)
37
+ end
38
+
39
+ def initial(&block)
40
+ @migrations['0000'] = block
41
+ end
42
+
43
+ def version(key, &block)
44
+ @migrations[key] = block
45
+ end
46
+ end
47
+
48
+ def run_schema_block(&)
49
+ SchemaBlockRunner.new(@migrations, &)
50
+ end
51
+
52
+ def execute_migrations(connection_pool)
53
+ connection_pool.with_db do |db|
54
+ current_version = get_schema_version(db)
55
+ migrations_keys = @migrations.keys.sort
56
+ migrations_keys.select { it > current_version } if current_version
57
+
58
+ migrations_keys.each do |key|
59
+ db.transaction do
60
+ @migrations[key].(db)
61
+ set_schema_version(db, key)
62
+ current_version = key
63
+ end
64
+ end
65
+
66
+ current_version
67
+ end
68
+ end
69
+
70
+ def get_schema_version(db)
71
+ db.execute <<~SQL
72
+ create table if not exists __syntropy_schema__(
73
+ k text primary key,
74
+ v text
75
+ );
76
+ SQL
77
+ db.query_single_splat <<~SQL
78
+ select v from __syntropy_schema__
79
+ where k = 'version'
80
+ SQL
81
+ end
82
+
83
+ def set_schema_version(db, version)
84
+ db.execute <<~SQL, v: version
85
+ insert into __syntropy_schema__ (k, v)
86
+ values ('version', :v)
87
+ on conflict(k) do update set v = :v
88
+ SQL
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ module DB
5
+ class Store
6
+ def initialize(connection_pool)
7
+ @connection_pool = connection_pool
8
+ end
9
+
10
+ def query(sql, *, **)
11
+ @connection_pool.with_db { it.query(sql, *, **) }
12
+ end
13
+
14
+ def query_single_row(sql, *, **)
15
+ @connection_pool.with_db { it.query_single(sql, *, **) }
16
+ end
17
+
18
+ def query_single_value(sql, *, **)
19
+ @connection_pool.with_db { it.query_single_splat(sql, *, **) }
20
+ end
21
+
22
+ def execute(sql, *, **)
23
+ @connection_pool.with_db { it.execute(sql, *, **) }
24
+ end
25
+
26
+ def transaction(&)
27
+ @connection_pool.with_db(&)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -4,7 +4,7 @@ TAG_DEBUG_PROC = ->(level, fn, line, col) {
4
4
  {
5
5
  'data-syntropy-level' => level,
6
6
  'data-syntropy-fn' => fn,
7
- 'data-syntropy-loc' => "vscode://file/#{fn}:#{line}:#{col}"
7
+ 'data-syntropy-loc' => "zed://file/#{fn}:#{line}:#{col}"
8
8
  }
9
9
  }
10
10
 
@@ -84,4 +84,10 @@ module Syntropy
84
84
 
85
85
  class BadRequestError < Error
86
86
  end
87
+
88
+ class InvalidRequestContentTypeError < Error
89
+ def http_status
90
+ HTTP::UNSUPPORTED_MEDIA_TYPE
91
+ end
92
+ end
87
93
  end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/http/client_connection'
4
+ require 'uri'
5
+
6
+ module Syntropy
7
+ module HTTP
8
+ class Client
9
+ def initialize(machine)
10
+ @machine = machine
11
+ end
12
+
13
+ def get(url, **headers, &)
14
+ uri = URI.parse(url)
15
+ headers = headers.merge(
16
+ ':method' => 'GET',
17
+ ':path' => uri.request_uri
18
+ )
19
+ req(uri, **headers, &)
20
+ end
21
+
22
+ private
23
+
24
+ # @param uri [URI]
25
+ def req(uri, **headers)
26
+ connection = make_connection(uri.scheme, uri.host, uri.port)
27
+ response_headers = connection.req(**headers)
28
+ if block_given?
29
+ yield(response_headers, connection)
30
+ else
31
+ [response_headers, connection.get_response_body(response_headers)]
32
+ end
33
+ end
34
+
35
+ def make_connection(_scheme, host, port)
36
+ ip = (host =~ /^\d+\.\d+\.\d+\.\d+$/) ? host : @machine.resolve(host)[0]
37
+
38
+ fd = @machine.tcp_connect(ip, port)
39
+ Syntropy::HTTP::ClientConnection.new(@machine, fd)
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/errors'
4
+ require 'syntropy/http/io_extensions'
5
+
6
+ module Syntropy
7
+ module HTTP
8
+ class ClientConnection
9
+ attr_reader :fd, :response_headers, :logger
10
+
11
+ def initialize(machine, fd, io_mode: :socket)
12
+ @machine = machine
13
+ @fd = fd
14
+ @io = machine.io(fd, io_mode)
15
+ end
16
+
17
+ def req(body: nil, **headers)
18
+ if body
19
+ headers = headers.merge(
20
+ 'Content-Length' => body.bytesize
21
+ )
22
+ end
23
+ @io.http_write_request_headers(**headers)
24
+ if body
25
+ @io.write(body)
26
+ end
27
+
28
+ @io.http_read_response_headers
29
+ end
30
+
31
+ def get_response_body(headers)
32
+ @io.http_read_body(headers)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,176 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/errors'
4
+
5
+ module Syntropy
6
+ module HTTP
7
+ module ProtocolMethods
8
+ RE_REQUEST_LINE = /^(get|head|options|trace|put|delete|post|patch|connect)\s+([^\s]+)\s+HTTP\/([019\.]{1,3})/i
9
+ RE_RESPONSE_LINE = /^HTTP\/1\.1\s+(\d{3})(\s+.+)?$/i
10
+ RE_HEADER_LINE = /^([a-z0-9\-]+):\s+(.+)/i
11
+
12
+ MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
13
+ MAX_RESPONSE_LINE_LEN = 1 << 8 # 256
14
+ MAX_HEADER_LINE_LEN = 1 << 13 # 8KB
15
+ MAX_CHUNK_SIZE_LEN = 16
16
+
17
+ # @return [Hash] headers
18
+ def http_read_request_headers
19
+ line = read_line(MAX_REQUEST_LINE_LEN)
20
+ return nil if !line
21
+
22
+ m = line.match(RE_REQUEST_LINE)
23
+ raise ProtocolError, 'Invalid request line' if !m
24
+
25
+ http_version = m[3]
26
+ raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
27
+
28
+ headers = {
29
+ ':method' => m[1].downcase,
30
+ ':path' => m[2]
31
+ }
32
+
33
+ loop do
34
+ line = read_line(MAX_HEADER_LINE_LEN)
35
+ break if line.nil? || line.empty?
36
+
37
+ m = line.match(RE_HEADER_LINE)
38
+ raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
39
+
40
+ headers[m[1].downcase] = m[2]
41
+ end
42
+
43
+ headers
44
+ end
45
+
46
+ def http_read_response_headers
47
+ line = read_line(MAX_RESPONSE_LINE_LEN)
48
+ return nil if !line
49
+
50
+ m = line.match(RE_RESPONSE_LINE)
51
+ raise ProtocolError, 'Invalid response line' if !m
52
+
53
+ headers = {
54
+ ':status' => m[1].to_i
55
+ }
56
+
57
+ loop do
58
+ line = read_line(MAX_HEADER_LINE_LEN)
59
+ break if line.nil? || line.empty?
60
+
61
+ m = line.match(RE_HEADER_LINE)
62
+ raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
63
+
64
+ k = m[1].downcase
65
+ if (h = headers[k])
66
+ (h = headers[k] = [h]) if !h.is_a?(Array)
67
+ h << m[2]
68
+ else
69
+ headers[k] = m[2]
70
+ end
71
+ end
72
+
73
+ headers
74
+ end
75
+
76
+ def http_read_body(headers)
77
+ content_length = headers['content-length']
78
+ if content_length
79
+ chunk = read(content_length.to_i)
80
+ return chunk
81
+ end
82
+
83
+ chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
84
+ if chunked_encoding
85
+ buf = +''
86
+ while (chunk = http_read_cte_chunk(nil))
87
+ buf << chunk
88
+ end
89
+ return buf
90
+ end
91
+
92
+ nil
93
+ end
94
+
95
+ def http_skip_body(headers)
96
+ content_length = headers['content-length']
97
+ if content_length
98
+ return skip(content_length.to_i)
99
+ end
100
+
101
+ chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
102
+ if chunked_encoding
103
+ while http_skip_cte_chunk
104
+ end
105
+ end
106
+
107
+ nil
108
+ end
109
+
110
+ def http_read_body_chunk(headers)
111
+ content_length = headers['content-length']
112
+ if content_length
113
+ chunk = read(content_length.to_i)
114
+ return chunk
115
+ end
116
+
117
+ chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
118
+ return http_read_cte_chunk(nil) if chunked_encoding
119
+
120
+ nil
121
+ end
122
+
123
+ def http_write_request_headers(headers)
124
+ method = headers[':method'] || (raise BadRequestError)
125
+ path = headers[':path'] || (raise BadRequestError)
126
+
127
+ lines = ["#{method} #{path} HTTP/1.1\r\n"]
128
+ headers.each do |k, v|
129
+ next if k =~ /^\:/
130
+
131
+ if v.is_a?(Array)
132
+ v.each { lines << "#{k}: #{it}\r\n" }
133
+ else
134
+ lines << "#{k}: #{v}\r\n"
135
+ end
136
+ end
137
+ lines << "\r\n"
138
+ write(*lines)
139
+ end
140
+
141
+ private
142
+
143
+ def http_read_cte_chunk(buffer)
144
+ chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
145
+ return nil if !chunk_size_str
146
+
147
+ chunk_size = chunk_size_str.to_i(16)
148
+ if chunk_size == 0
149
+ read_line(0)
150
+ return nil
151
+ end
152
+
153
+ chunk = read(chunk_size)
154
+ read_line(0)
155
+
156
+ buffer ? (buffer << chunk) : chunk
157
+ end
158
+
159
+ def http_skip_cte_chunk
160
+ chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
161
+ return if !chunk_size_str
162
+
163
+ chunk_size = chunk_size_str.to_i(16)
164
+ if chunk_size == 0
165
+ read_line(0)
166
+ return nil
167
+ end
168
+
169
+ chunk = skip(chunk_size)
170
+ read_line(0)
171
+ end
172
+ end
173
+ end
174
+ end
175
+
176
+ UringMachine::IO.include(Syntropy::HTTP::ProtocolMethods)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'syntropy/http/connection'
3
+ require 'syntropy/http/server_connection'
4
4
 
5
5
  module Syntropy
6
6
  module HTTP
@@ -119,13 +119,13 @@ module Syntropy
119
119
  end
120
120
 
121
121
  def accept_incoming(listen_fd)
122
- @machine.accept_each(listen_fd) { start_client_connection(it) }
122
+ @machine.accept_each(listen_fd) { start_connection(it) }
123
123
  rescue UM::Terminate
124
- # terminated
124
+ @machine.shutdown(listen_fd, UM::SHUT_RD)
125
125
  end
126
126
 
127
- def start_client_connection(fd)
128
- conn = Connection.new(self, @machine, fd, @env, &@app)
127
+ def start_connection(fd)
128
+ conn = ServerConnection.new(@machine, fd, @env, &@app)
129
129
  f = @machine.spin(conn) do
130
130
  it.run
131
131
  ensure