syntropy 0.35.0 → 0.37.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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +20 -0
  3. data/TODO.md +0 -2
  4. data/cmd/new/template/.gitignore +2 -1
  5. data/cmd/new/template/Gemfile +1 -1
  6. data/cmd/new/template/app/_lib/storage.rb +13 -0
  7. data/cmd/new/template/config/Caddyfile +5 -0
  8. data/cmd/new/template/docker-compose.yml +28 -3
  9. data/cmd/new.rb +7 -1
  10. data/cmd/serve.rb +3 -1
  11. data/examples/basic/counter_api.rb +1 -1
  12. data/examples/blog/app/_lib/posts.rb +3 -3
  13. data/examples/blog/app/_lib/storage.rb +13 -0
  14. data/lib/syntropy/http/io_extensions.rb +9 -0
  15. data/lib/syntropy/http/server_connection.rb +1 -0
  16. data/lib/syntropy/json_api.rb +5 -0
  17. data/lib/syntropy/module_loader.rb +1 -3
  18. data/lib/syntropy/{db → storage}/connection_pool.rb +2 -1
  19. data/lib/syntropy/storage/kv_store.rb +68 -0
  20. data/lib/syntropy/storage/prepared_query.rb +36 -0
  21. data/lib/syntropy/{db → storage}/schema.rb +4 -4
  22. data/lib/syntropy/{db → storage}/store.rb +1 -1
  23. data/lib/syntropy/storage.rb +6 -0
  24. data/lib/syntropy/test.rb +4 -2
  25. data/lib/syntropy/version.rb +1 -1
  26. data/lib/syntropy.rb +5 -7
  27. data/test/fixtures/app/_lib/klass.rb +1 -1
  28. data/test/fixtures/app/api+.rb +1 -1
  29. data/test/{test_db_connection_pool.rb → test_connection_pool.rb} +4 -4
  30. data/test/test_http_protocol.rb +54 -0
  31. data/test/test_kv_store.rb +84 -0
  32. data/test/test_module_loader.rb +1 -2
  33. data/test/{test_db_schema.rb → test_schema.rb} +5 -5
  34. data/test/{test_db_store.rb → test_store.rb} +3 -3
  35. metadata +14 -32
  36. data/cmd/new/template/app/_lib/database.rb +0 -13
  37. data/cmd/new/template_old/site/.gitignore +0 -57
  38. data/cmd/new/template_old/site/Dockerfile +0 -32
  39. data/cmd/new/template_old/site/Gemfile +0 -3
  40. data/cmd/new/template_old/site/README.md +0 -0
  41. data/cmd/new/template_old/site/bin/console +0 -0
  42. data/cmd/new/template_old/site/bin/restart +0 -0
  43. data/cmd/new/template_old/site/bin/server +0 -0
  44. data/cmd/new/template_old/site/bin/start +0 -0
  45. data/cmd/new/template_old/site/bin/stop +0 -0
  46. data/cmd/new/template_old/site/docker-compose.yml +0 -51
  47. data/cmd/new/template_old/site/proxy/Dockerfile +0 -5
  48. data/cmd/new/template_old/site/proxy/etc/Caddyfile +0 -7
  49. data/cmd/new/template_old/site/proxy/etc/tls_auto +0 -2
  50. data/cmd/new/template_old/site/proxy/etc/tls_cloudflare +0 -4
  51. data/cmd/new/template_old/site/proxy/etc/tls_custom +0 -1
  52. data/cmd/new/template_old/site/proxy/etc/tls_selfsigned +0 -1
  53. data/cmd/new/template_old/site/site/_layout/default.rb +0 -11
  54. data/cmd/new/template_old/site/site/about.md +0 -6
  55. data/cmd/new/template_old/site/site/articles/cage.rb +0 -29
  56. data/cmd/new/template_old/site/site/articles/index.rb +0 -3
  57. data/cmd/new/template_old/site/site/assets/css/style.css +0 -40
  58. data/cmd/new/template_old/site/site/assets/img/syntropy.png +0 -0
  59. data/cmd/new/template_old/site/site/index.rb +0 -15
  60. data/examples/blog/app/_lib/database.rb +0 -13
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 90b51e20e3a93a2b92bca677eddfe21efdd929d7378499f012dfb4c733336ddd
4
- data.tar.gz: 3f873ae675918b8601f8144daca4041b7f90c490dcdda1d32143ad9c1ab13d75
3
+ metadata.gz: eb6c66cfca4b2d7be9060c5571028b5356950fb34f8fba634b0d43b97169cd48
4
+ data.tar.gz: 447b2442d22332e0642869e01eee03889d8c85ba31e58909d603382be51f4c7d
5
5
  SHA512:
6
- metadata.gz: 40a3b22e7d24ab92be72b5552702ec5e6ca878ea8731537d552323e3dca5252b0ce965add0270a6bda710e8d20d32478060adee740f7acc33e386255daf44882
7
- data.tar.gz: 4129035e2daca7068eda05ced8b78604b10fbc39fd67aae20da3de5eb53b880b395c47d70795ec39e282cca50e65e2b3a878853ff8fd2e47e95bd2cb7cf80711
6
+ metadata.gz: 19d092f17b75b7cc3290cb2802f46bd19efbb30855f3acf78e1cfc2f88084e0c0dca68bbfcd661545bd50def484cc9526d8a2f4057c9a6da8d6639c95b2cd8c4
7
+ data.tar.gz: 34d839e355da94f8993ffefa1a48c9bebfecb6da9c6b43cfe7f934d4fe8a804889d2a7a75874f0107b3226bd6a880037466610f4cffd87d48bdab2f19c5e0545
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ # 0.37.0 2026-06-07
2
+
3
+ - Call `IO#clear` before closing server connection
4
+ - syntropy new:
5
+ - Remove socket mapping for backend in docker-compose.yml
6
+ - Add caddy reverse proxy to template
7
+ - Add overwrite confirmation, better file copying
8
+ - Verify storage module `migrate!` method exists
9
+ - Fix `set_schema_version` for usage in PG DB
10
+ - Do not raise exception on missing config module
11
+ - Do not convert class export value to class instance, allow exporting a
12
+ class
13
+ - Fix HTTP protocol error when pipelining post requests with empty body
14
+ - Setup fiber scheduler when running server
15
+
16
+ # 0.36.0 2026-06-04
17
+
18
+ - Rename `DB` to `Storage`
19
+ - Add `PreparedQuery`, `KVStore`
20
+
1
21
  # 0.35.0 2026-06-04
2
22
 
3
23
  - Add `syntropy new` command
data/TODO.md CHANGED
@@ -40,8 +40,6 @@
40
40
  - [v] Frontend part of JSON API
41
41
  - [v] Auto-refresh page when file changes
42
42
  - [ ] SQLite database capabilities
43
- - [ ] Model API + tools
44
- - [ ] Do we need/want migrations?
45
43
  - [ ] Stores
46
44
  - [ ] KV store (with TTL)
47
45
  - [v] Examples
@@ -1,3 +1,4 @@
1
1
  Gemfile.lock
2
- storage/*
2
+ storage/*.db*
3
+ storage/caddy/*
3
4
  vendor/bundle
@@ -1,3 +1,3 @@
1
1
  source 'https://gem.coop'
2
2
 
3
- gem 'syntropy', '~>0.34.4'
3
+ gem 'syntropy', '~>0.36.0'
@@ -0,0 +1,13 @@
1
+ export self
2
+
3
+ def connection_pool
4
+ @connection_pool ||= Storage::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
+ end
6
+
7
+ def schema
8
+ Storage::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
+ end
10
+
11
+ def migrate!
12
+ schema.apply(connection_pool)
13
+ end
@@ -0,0 +1,5 @@
1
+ localhost {
2
+ reverse_proxy app_server:1234
3
+ tls internal
4
+ encode
5
+ }
@@ -7,17 +7,18 @@ services:
7
7
  - seccomp:unconfined
8
8
  volumes:
9
9
  - .:/syntropy
10
- ports:
11
- - "1234:1234"
12
10
  stop_signal: SIGINT
13
11
  stop_grace_period: 10s
14
- restart: always
12
+ restart: unless-stopped
15
13
  healthcheck:
16
14
  test: "curl 'http://localhost:1234/'"
17
15
  interval: "30s"
18
16
  timeout: "3s"
19
17
  start_period: "5s"
20
18
  retries: 3
19
+ networks:
20
+ - proxy_network
21
+
21
22
  console:
22
23
  build: .
23
24
  command: bundle exec syntropy console
@@ -31,6 +32,7 @@ services:
31
32
  - .:/syntropy
32
33
  stop_signal: SIGINT
33
34
  restart: never
35
+
34
36
  test:
35
37
  build: .
36
38
  command: bundle exec syntropy test -w
@@ -44,3 +46,26 @@ services:
44
46
  - .:/syntropy
45
47
  stop_signal: SIGINT
46
48
  restart: never
49
+
50
+ proxy:
51
+ depends_on:
52
+ - app_server
53
+ image: caddy:2-alpine
54
+ build:
55
+ context: ./proxy
56
+ dockerfile: Dockerfile
57
+ restart: unless-stopped
58
+ ports:
59
+ - "80:80"
60
+ - "443:443"
61
+ - "443:443/udp"
62
+ volumes:
63
+ - ./config/Caddyfile:/etc/caddy/Caddyfile
64
+ - ./storage/caddy/data:/data
65
+ - ./storage/caddy/config:/config
66
+ networks:
67
+ - proxy_network
68
+
69
+ networks:
70
+ proxy_network:
71
+ name: proxy_network
data/cmd/new.rb CHANGED
@@ -3,9 +3,15 @@
3
3
  require 'optparse'
4
4
  require 'fileutils'
5
5
 
6
+ opts = {}
7
+
6
8
  parser = OptionParser.new do |o|
7
9
  o.banner = 'Usage: syntropy new NAME [options]'
8
10
 
11
+ o.on('-y', '--yes', 'Confirm all overwrites') do
12
+ opts[:yes] = true
13
+ end
14
+
9
15
  o.on('-h', '--help', 'Show this help message') do
10
16
  puts o
11
17
  exit
@@ -37,7 +43,7 @@ template_path = File.join(__dir__, 'new/template')
37
43
 
38
44
  begin
39
45
  `mkdir -p "#{path}"`
40
- `cp -r #{template_path}/* "#{path}/"`
46
+ system("cp -rv#{opts[:yes] ? '' : 'i'} #{template_path}/* \"#{path}/\"")
41
47
  puts "Your app is ready in #{path}"
42
48
  rescue => e
43
49
  p e
data/cmd/serve.rb CHANGED
@@ -94,7 +94,9 @@ env[:banner] = false
94
94
  env[:machine] = Syntropy.machine = UM.new
95
95
  env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
96
96
 
97
- require 'syntropy/version'
97
+ require 'uringmachine/fiber_scheduler'
98
+ Fiber.set_scheduler(UM::FiberScheduler.new(env[:machine]))
99
+
98
100
  require 'syntropy/dev_mode' if Syntropy.dev_mode
99
101
 
100
102
  app = Syntropy::App.load(env)
@@ -19,4 +19,4 @@ class CounterAPI < Syntropy::JSONAPI
19
19
  end
20
20
  end
21
21
 
22
- export CounterAPI
22
+ export CounterAPI.new(@env)
@@ -1,6 +1,6 @@
1
- DB = import '/_lib/database'
1
+ Storage = import '/_lib/storage'
2
2
 
3
- class PostStore < Syntropy::DB::Store
3
+ class PostStore < Syntropy::Storage::Store
4
4
  # @return [Integer] post id
5
5
  def create(title, body)
6
6
  query_single_value <<~SQL, title:, body:
@@ -46,4 +46,4 @@ class PostStore < Syntropy::DB::Store
46
46
  end
47
47
  end
48
48
 
49
- export PostStore.new(DB.connection_pool)
49
+ export PostStore.new(Storage.connection_pool)
@@ -0,0 +1,13 @@
1
+ export self
2
+
3
+ def connection_pool
4
+ @connection_pool ||= Storage::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
+ end
6
+
7
+ def schema
8
+ Storage::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
+ end
10
+
11
+ def migrate!
12
+ schema.apply(connection_pool)
13
+ end
@@ -76,6 +76,9 @@ module Syntropy
76
76
  def http_read_body(headers)
77
77
  content_length = headers['content-length']
78
78
  if content_length
79
+ content_length = content_length.to_i
80
+ return nil if content_length == 0
81
+
79
82
  chunk = read(content_length.to_i)
80
83
  return chunk
81
84
  end
@@ -95,6 +98,9 @@ module Syntropy
95
98
  def http_skip_body(headers)
96
99
  content_length = headers['content-length']
97
100
  if content_length
101
+ content_length = content_length.to_i
102
+ return if content_length == 0
103
+
98
104
  return skip(content_length.to_i)
99
105
  end
100
106
 
@@ -110,6 +116,9 @@ module Syntropy
110
116
  def http_read_body_chunk(headers)
111
117
  content_length = headers['content-length']
112
118
  if content_length
119
+ content_length = content_length.to_i
120
+ return nil if content_length == 0
121
+
113
122
  chunk = read(content_length.to_i)
114
123
  return chunk
115
124
  end
@@ -39,6 +39,7 @@ module Syntropy
39
39
  error: e
40
40
  )
41
41
  ensure
42
+ @io.clear
42
43
  @machine.close_async(@fd)
43
44
  end
44
45
 
@@ -25,6 +25,11 @@ module Syntropy
25
25
  ':status' => status,
26
26
  'Content-Type' => 'application/json'
27
27
  )
28
+ rescue => e
29
+ puts '*' * 40
30
+ p e
31
+ p e.backtrace.join
32
+ puts
28
33
  end
29
34
 
30
35
  private
@@ -167,8 +167,6 @@ module Syntropy
167
167
  raise Syntropy::Error, "No export found in #{fn}" if raise_on_missing
168
168
  when String
169
169
  ->(req) { req.respond(export_value) }
170
- when Class
171
- export_value.new(@env)
172
170
  else
173
171
  export_value
174
172
  end
@@ -200,7 +198,7 @@ module Syntropy
200
198
  m
201
199
  rescue SyntaxError => e
202
200
  env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
203
- STDERR.puts("\n#{e.message}")
201
+ STDERR.puts("\n#{e.message}") if !Syntropy.test_mode
204
202
 
205
203
  if (m = e.message.match(/^(.+)\: syntax/))
206
204
  location = m[1]
@@ -3,7 +3,8 @@
3
3
  require 'extralite'
4
4
 
5
5
  module Syntropy
6
- module DB
6
+ module Storage
7
+ # ConnectionPool implements concurrent access to an SQLite database.
7
8
  class ConnectionPool
8
9
  attr_reader :count
9
10
 
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/storage/store'
4
+
5
+ module Syntropy
6
+ module Storage
7
+ # The KVStore class implements an SQLite-backed key-value store
8
+ class KVStore < Store
9
+ attr_reader :q_get, :q_set
10
+
11
+ def self.apply_schema(db, table_name)
12
+ db.execute <<~SQL
13
+ create table if not exists #{table_name} (key text primary key, value, expires float);
14
+ create index if not exists idx_#{table_name}_expires on #{table_name} (expires) where expires is not null;
15
+ SQL
16
+ end
17
+
18
+ def initialize(connection_pool, table_name)
19
+ super(connection_pool)
20
+ @table_name = table_name
21
+
22
+ setup_queries
23
+ end
24
+
25
+ def get(db, key)
26
+ db[@q_get].bind(key).next
27
+ end
28
+
29
+ def set(db, key, value)
30
+ db[@q_set].execute(key, value)
31
+ end
32
+
33
+ def setex(db, key, value, ttl)
34
+ db[@q_setex].execute(key, value, ttl ? Time.now.to_f + ttl : nil)
35
+ end
36
+
37
+ def sweep(db)
38
+ db[@q_sweep].execute(Time.now.to_f)
39
+ end
40
+
41
+ private
42
+
43
+ def setup_queries
44
+ @q_get = Storage.prepare_splat <<~SQL
45
+ select value from #{@table_name}
46
+ where key = ?
47
+ SQL
48
+
49
+ @q_set = Storage.prepare <<~SQL
50
+ insert into #{@table_name} (key, value)
51
+ values($1, $2)
52
+ on conflict (key) do update set value = $2, expires = null
53
+ SQL
54
+
55
+ @q_setex = Storage.prepare <<~SQL
56
+ insert into #{@table_name} (key, value, expires)
57
+ values($1, $2, $3)
58
+ on conflict (key) do update set value = $2, expires = $3
59
+ SQL
60
+
61
+ @q_sweep = Storage.prepare <<~SQL
62
+ delete from #{@table_name}
63
+ where expires < ?
64
+ SQL
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extralite'
4
+
5
+ module Syntropy
6
+ module Storage
7
+ class << self
8
+ def prepare(sql)
9
+ PreparedQuery.new(sql)
10
+ end
11
+
12
+ def prepare_splat(sql)
13
+ PreparedQuery.new(sql, :prepare_splat)
14
+ end
15
+ end
16
+
17
+ # Represents information about a prepared query
18
+ class PreparedQuery
19
+ attr_reader :sql, :mode
20
+
21
+ def initialize(sql, mode = :prepare)
22
+ @sql = sql
23
+ @mode = mode
24
+ end
25
+ end
26
+
27
+ # Extensions for Extralite::Database
28
+ module ExtraliteDatabaseExtensions
29
+ def [](pq)
30
+ (@prepared_queries ||= {})[pq] ||= send(pq.mode, pq.sql)
31
+ end
32
+ end
33
+
34
+ ::Extralite::Database.include(ExtraliteDatabaseExtensions)
35
+ end
36
+ end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- module DB
4
+ module Storage
5
5
  class Schema
6
6
  def initialize(module_loader: nil, schema_root: '_schema', &)
7
7
  @migrations = {}
@@ -81,10 +81,10 @@ module Syntropy
81
81
  end
82
82
 
83
83
  def set_schema_version(db, version)
84
- db.execute <<~SQL, v: version
84
+ db.execute <<~SQL, version
85
85
  insert into __syntropy_schema__ (k, v)
86
- values ('version', :v)
87
- on conflict(k) do update set v = :v
86
+ values ('version', $1)
87
+ on conflict(k) do update set v = $1
88
88
  SQL
89
89
  end
90
90
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- module DB
4
+ module Storage
5
5
  # The Store class represents a data store based on one or more tables, and
6
6
  # connected to a database connection pool.
7
7
  class Store
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/storage/connection_pool'
4
+ require 'syntropy/storage/schema'
5
+ require 'syntropy/storage/store'
6
+ require 'syntropy/storage/prepared_query'
data/lib/syntropy/test.rb CHANGED
@@ -118,8 +118,8 @@ module Syntropy
118
118
  )
119
119
  @test_harness = Syntropy::TestHarness.new(@app)
120
120
 
121
- @db = load_module('/_lib/database', raise_on_missing: false)
122
- @db&.migrate!
121
+ @db = load_module('/_lib/storage', raise_on_missing: false)
122
+ @db&.migrate! if @db.respond_to?(:migrate!)
123
123
  end
124
124
 
125
125
  # Cleans up a test instance.
@@ -241,3 +241,5 @@ module Syntropy
241
241
 
242
242
  Request.include TestRequestExtensions
243
243
  end
244
+
245
+ Syntropy.test_mode = true
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.35.0'
4
+ VERSION = '0.37.0'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -9,9 +9,7 @@ require 'syntropy/logger'
9
9
  require 'syntropy/http'
10
10
  require 'syntropy/mime_types'
11
11
  require 'syntropy/app'
12
- require 'syntropy/db/connection_pool'
13
- require 'syntropy/db/schema'
14
- require 'syntropy/db/store'
12
+ require 'syntropy/storage'
15
13
  require 'syntropy/errors'
16
14
  require 'syntropy/markdown'
17
15
  require 'syntropy/module_loader'
@@ -29,7 +27,7 @@ module Syntropy
29
27
  extend Utilities
30
28
 
31
29
  class << self
32
- attr_accessor :machine, :dev_mode
30
+ attr_accessor :machine, :dev_mode, :test_mode
33
31
 
34
32
  # Runs the given block on a separate thread. Use this method for running
35
33
  # code that is not fiber-aware (i.e. does not use UringMachine).
@@ -85,9 +83,9 @@ module Syntropy
85
83
  logger: nil
86
84
  )
87
85
  loader = ModuleLoader.new(loader_env)
88
- config = loader.load(env[:mode])
89
-
90
- env[:config] = config
86
+ if (config = loader.load(env[:mode], raise_on_missing: false))
87
+ env[:config] = config
88
+ end
91
89
  end
92
90
 
93
91
  private
@@ -12,4 +12,4 @@ class Klass
12
12
  end
13
13
  end
14
14
 
15
- export Klass
15
+ export Klass.new(@env)
@@ -21,4 +21,4 @@ class API < Syntropy::JSONAPI
21
21
  end
22
22
  end
23
23
 
24
- export API
24
+ export API.new(@env)
@@ -2,11 +2,11 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class DBConnectionPoolTest < Minitest::Test
5
+ class ConnectionPoolTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
- @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
9
+ @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
10
10
 
11
11
  FileUtils.rm(@fn) rescue nil
12
12
  @standalone_db = Extralite::Database.new(@fn)
@@ -19,7 +19,7 @@ class DBConnectionPoolTest < Minitest::Test
19
19
  @cp.close
20
20
  end
21
21
 
22
- def test_with_db
22
+ def test_connection_pool_with_db
23
23
  assert_equal 0, @cp.count
24
24
 
25
25
  @cp.with_db do |db|
@@ -74,7 +74,7 @@ class DBConnectionPoolTest < Minitest::Test
74
74
  assert_equal 4, @cp.count
75
75
  end
76
76
 
77
- def test_with_db_reentrant
77
+ def test_connection_pool_with_db_reentrant
78
78
  dbs = @cp.with_db do |db1|
79
79
  @cp.with_db do |db2|
80
80
  [db1, db2]
@@ -248,3 +248,57 @@ class HTTPProtocolResponseTest < HTTPProtocolTest
248
248
  assert_raises(Syntropy::ProtocolError) { @io.http_read_response_headers }
249
249
  end
250
250
  end
251
+
252
+ class PipelineTest < HTTPProtocolTest
253
+ def test_pipeline_post_zero_content_length
254
+ msg = "POST /counter_api?q=incr HTTP/1.1\r\n" +
255
+ "Host: localhost:1234\r\n" +
256
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0\r\n" +
257
+ "Accept: */*\r\n" +
258
+ "Accept-Language: en-US,en;q=0.9\r\n" +
259
+ "Accept-Encoding: gzip, deflate, br, zstd\r\n" +
260
+ "Referer: http://localhost:1234/counter\r\n" +
261
+ "Origin: http://localhost:1234\r\n" +
262
+ "Connection: keep-alive\r\n" +
263
+ "Sec-Fetch-Dest: empty\r\n" +
264
+ "Sec-Fetch-Mode: cors\r\n" +
265
+ "Sec-Fetch-Site: same-origin\r\n" +
266
+ "Priority: u=0\r\nPragma: no-cache\r\n" +
267
+ "Cache-Control: no-cache\r\n" +
268
+ "Content-Length: 0\r\n\r\n"
269
+
270
+ write(msg * 3)
271
+ 3.times {
272
+ h = @io.http_read_request_headers
273
+ assert_equal '*/*', h['accept']
274
+ assert_nil @io.http_read_body_chunk(h)
275
+ }
276
+ end
277
+
278
+ def test_pipeline_post_with_body
279
+ msg = "POST /counter_api?q=incr HTTP/1.1\r\n" +
280
+ "Host: localhost:1234\r\n" +
281
+ "User-Agent: Mozilla/5.0 (X11; Linux x86_64; rv:151.0) Gecko/20100101 Firefox/151.0\r\n" +
282
+ "Accept: */*\r\n" +
283
+ "Accept-Language: en-US,en;q=0.9\r\n" +
284
+ "Accept-Encoding: gzip, deflate, br, zstd\r\n" +
285
+ "Referer: http://localhost:1234/counter\r\n" +
286
+ "Origin: http://localhost:1234\r\n" +
287
+ "Connection: keep-alive\r\n" +
288
+ "Sec-Fetch-Dest: empty\r\n" +
289
+ "Sec-Fetch-Mode: cors\r\n" +
290
+ "Sec-Fetch-Site: same-origin\r\n" +
291
+ "Priority: u=0\r\nPragma: no-cache\r\n" +
292
+ "Cache-Control: no-cache\r\n" +
293
+ "Content-Length: 3\r\n\r\n" +
294
+ "abc"
295
+
296
+ write(msg * 3)
297
+ 3.times {
298
+ h = @io.http_read_request_headers
299
+ assert_equal '*/*', h['accept']
300
+ assert_equal 'abc', @io.http_read_body_chunk(h)
301
+ }
302
+
303
+ end
304
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'syntropy/storage/kv_store'
5
+
6
+ class KVStoreTest < Minitest::Test
7
+ def setup
8
+ @machine = UM.new
9
+ @fn = "/tmp/#{rand(100000)}.db"
10
+ FileUtils.rm(@fn) rescue nil
11
+ @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
12
+ end
13
+
14
+ def teardown
15
+ @cp.close
16
+ end
17
+
18
+ def test_connection_pool_prepare
19
+ pq = Syntropy::Storage.prepare('select ? as a, 42 as b')
20
+ assert_kind_of Syntropy::Storage::PreparedQuery, pq
21
+ assert_equal 'select ? as a, 42 as b', pq.sql
22
+ assert_equal :prepare, pq.mode
23
+
24
+ assert_kind_of Extralite::Query, @cp.with_db { it[pq] }
25
+ assert_equal [{ a: 'foo', b: 42 }], @cp.with_db { it[pq].bind('foo').to_a }
26
+ end
27
+
28
+ def test_connection_pool_prepare_splat
29
+ pq = Syntropy::Storage.prepare_splat('select ?')
30
+ assert_kind_of Syntropy::Storage::PreparedQuery, pq
31
+ assert_equal 'select ?', pq.sql
32
+ assert_equal :prepare_splat, pq.mode
33
+
34
+ assert_kind_of Extralite::Query, @cp.with_db { it[pq] }
35
+ assert_equal ['foo'], @cp.with_db { it[pq].bind('foo').to_a }
36
+ end
37
+
38
+ def test_kv_store_apply_schema
39
+ assert_respond_to Syntropy::Storage::KVStore, :apply_schema
40
+
41
+ assert_raises(Extralite::SQLError) { @cp.query('select * from kv') }
42
+ Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
43
+ assert_equal [], @cp.query('select * from kv')
44
+ end
45
+
46
+ def test_kv_store_get_set
47
+ Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
48
+ kv_store = Syntropy::Storage::KVStore.new(@cp, 'kv')
49
+
50
+ @cp.with_db do |db|
51
+ assert_nil kv_store.get(db, 'foo')
52
+ assert_nil kv_store.get(db, 'bar')
53
+
54
+ kv_store.set(db, 'foo', '123')
55
+
56
+ assert_equal '123', kv_store.get(db, 'foo')
57
+ assert_nil kv_store.get(db, 'bar')
58
+
59
+ kv_store.set(db, 'bar', '456')
60
+ assert_equal '123', kv_store.get(db, 'foo')
61
+ assert_equal '456', kv_store.get(db, 'bar')
62
+ end
63
+ end
64
+
65
+ def test_kv_store_setex_sweep
66
+ Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
67
+ kv_store = Syntropy::Storage::KVStore.new(@cp, 'kv')
68
+
69
+ @cp.with_db do |db|
70
+ kv_store.set(db, 'foo', '123')
71
+ kv_store.setex(db, 'bar', '456', 0.05)
72
+ assert_equal 0, kv_store.sweep(db)
73
+
74
+ assert_equal '123', kv_store.get(db, 'foo')
75
+ assert_equal '456', kv_store.get(db, 'bar')
76
+
77
+ sleep 0.1
78
+ assert_equal 1, kv_store.sweep(db)
79
+
80
+ assert_equal '123', kv_store.get(db, 'foo')
81
+ assert_nil kv_store.get(db, 'bar')
82
+ end
83
+ end
84
+ end
@@ -25,8 +25,7 @@ class ModuleTest < Minitest::Test
25
25
 
26
26
  mod = @loader.load('_lib/klass')
27
27
  assert_equal :bar, mod.foo
28
- @env[:baz] += 1
29
- assert_equal 43, mod.bar
28
+ assert_equal 42, mod.bar
30
29
  end
31
30
 
32
31
  def test_import_paths
@@ -2,12 +2,12 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class DBSchemaTest < Minitest::Test
5
+ class SchemaTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
9
  FileUtils.rm(@fn) rescue nil
10
- @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
10
+ @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
11
11
  end
12
12
 
13
13
  def teardown
@@ -15,7 +15,7 @@ class DBSchemaTest < Minitest::Test
15
15
  end
16
16
 
17
17
  def test_db_schema_initial
18
- schema = Syntropy::DB::Schema.new do
18
+ schema = Syntropy::Storage::Schema.new do
19
19
  initial do |db|
20
20
  db.execute <<~SQL
21
21
  create table posts (
@@ -35,7 +35,7 @@ class DBSchemaTest < Minitest::Test
35
35
  end
36
36
 
37
37
  def test_db_schema_version_blocks
38
- schema = Syntropy::DB::Schema.new do
38
+ schema = Syntropy::Storage::Schema.new do
39
39
  initial do |db|
40
40
  db.execute <<~SQL
41
41
  create table posts (
@@ -79,7 +79,7 @@ class DBSchemaTest < Minitest::Test
79
79
  module_loader = Syntropy::ModuleLoader.new({
80
80
  app_root: File.join(__dir__, 'fixtures/schema')
81
81
  })
82
- schema = Syntropy::DB::Schema.new(module_loader:, schema_root: '/')
82
+ schema = Syntropy::Storage::Schema.new(module_loader:, schema_root: '/')
83
83
 
84
84
  assert_nil schema.current_version(@cp)
85
85
  schema.apply(@cp)
@@ -2,12 +2,12 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class DBStoreTest < Minitest::Test
5
+ class StoreTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
9
  FileUtils.rm(@fn) rescue nil
10
- @cp = Syntropy::DB::ConnectionPool.new(@machine, @fn, 4)
10
+ @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
11
11
  end
12
12
 
13
13
  def teardown
@@ -15,7 +15,7 @@ class DBStoreTest < Minitest::Test
15
15
  end
16
16
 
17
17
  def test_db_store
18
- store = Syntropy::DB::Store.new(@cp)
18
+ store = Syntropy::Storage::Store.new(@cp)
19
19
 
20
20
  assert_equal [{a: 42}], store.query("select ? as a", 42)
21
21
  assert_equal({a: 42}, store.query_single_row("select ? as a", 42))
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.35.0
4
+ version: 0.37.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -176,39 +176,17 @@ files:
176
176
  - cmd/new/template/Gemfile
177
177
  - cmd/new/template/README.md
178
178
  - cmd/new/template/app/_layout/default.rb
179
- - cmd/new/template/app/_lib/database.rb
179
+ - cmd/new/template/app/_lib/storage.rb
180
180
  - cmd/new/template/app/_schema/2026-01-01-initial.rb
181
181
  - cmd/new/template/app/assets/style.css
182
182
  - cmd/new/template/app/index.rb
183
183
  - cmd/new/template/app/test.rb
184
+ - cmd/new/template/config/Caddyfile
184
185
  - cmd/new/template/config/development.rb
185
186
  - cmd/new/template/config/production.rb
186
187
  - cmd/new/template/config/test.rb
187
188
  - cmd/new/template/docker-compose.yml
188
189
  - cmd/new/template/test/test_app.rb
189
- - cmd/new/template_old/site/.gitignore
190
- - cmd/new/template_old/site/Dockerfile
191
- - cmd/new/template_old/site/Gemfile
192
- - cmd/new/template_old/site/README.md
193
- - cmd/new/template_old/site/bin/console
194
- - cmd/new/template_old/site/bin/restart
195
- - cmd/new/template_old/site/bin/server
196
- - cmd/new/template_old/site/bin/start
197
- - cmd/new/template_old/site/bin/stop
198
- - cmd/new/template_old/site/docker-compose.yml
199
- - cmd/new/template_old/site/proxy/Dockerfile
200
- - cmd/new/template_old/site/proxy/etc/Caddyfile
201
- - cmd/new/template_old/site/proxy/etc/tls_auto
202
- - cmd/new/template_old/site/proxy/etc/tls_cloudflare
203
- - cmd/new/template_old/site/proxy/etc/tls_custom
204
- - cmd/new/template_old/site/proxy/etc/tls_selfsigned
205
- - cmd/new/template_old/site/site/_layout/default.rb
206
- - cmd/new/template_old/site/site/about.md
207
- - cmd/new/template_old/site/site/articles/cage.rb
208
- - cmd/new/template_old/site/site/articles/index.rb
209
- - cmd/new/template_old/site/site/assets/css/style.css
210
- - cmd/new/template_old/site/site/assets/img/syntropy.png
211
- - cmd/new/template_old/site/site/index.rb
212
190
  - cmd/serve.rb
213
191
  - cmd/test.rb
214
192
  - docker-compose.yml
@@ -222,8 +200,8 @@ files:
222
200
  - examples/basic/templates.rb
223
201
  - examples/blog/.gitignore
224
202
  - examples/blog/app/_layout/default.rb
225
- - examples/blog/app/_lib/database.rb
226
203
  - examples/blog/app/_lib/posts.rb
204
+ - examples/blog/app/_lib/storage.rb
227
205
  - examples/blog/app/_schema/2026-01-01-initial.rb
228
206
  - examples/blog/app/assets/style.css
229
207
  - examples/blog/app/index.rb
@@ -262,9 +240,6 @@ files:
262
240
  - lib/syntropy/applets/builtin/json_api.js
263
241
  - lib/syntropy/applets/builtin/ping.rb
264
242
  - lib/syntropy/applets/builtin/req.rb
265
- - lib/syntropy/db/connection_pool.rb
266
- - lib/syntropy/db/schema.rb
267
- - lib/syntropy/db/store.rb
268
243
  - lib/syntropy/dev_mode.rb
269
244
  - lib/syntropy/errors.rb
270
245
  - lib/syntropy/http.rb
@@ -288,6 +263,12 @@ files:
288
263
  - lib/syntropy/routing_tree.rb
289
264
  - lib/syntropy/session.rb
290
265
  - lib/syntropy/side_run.rb
266
+ - lib/syntropy/storage.rb
267
+ - lib/syntropy/storage/connection_pool.rb
268
+ - lib/syntropy/storage/kv_store.rb
269
+ - lib/syntropy/storage/prepared_query.rb
270
+ - lib/syntropy/storage/schema.rb
271
+ - lib/syntropy/storage/store.rb
291
272
  - lib/syntropy/test.rb
292
273
  - lib/syntropy/utils.rb
293
274
  - lib/syntropy/version.rb
@@ -340,23 +321,24 @@ files:
340
321
  - test/run.rb
341
322
  - test/test_app.rb
342
323
  - test/test_caching.rb
343
- - test/test_db_connection_pool.rb
344
- - test/test_db_schema.rb
345
- - test/test_db_store.rb
324
+ - test/test_connection_pool.rb
346
325
  - test/test_errors.rb
347
326
  - test/test_http_client.rb
348
327
  - test/test_http_client_connection.rb
349
328
  - test/test_http_protocol.rb
350
329
  - test/test_http_server_connection.rb
351
330
  - test/test_json_api.rb
331
+ - test/test_kv_store.rb
352
332
  - test/test_mock_adapter.rb
353
333
  - test/test_module_loader.rb
354
334
  - test/test_request.rb
355
335
  - test/test_request_session.rb
356
336
  - test/test_response.rb
357
337
  - test/test_routing_tree.rb
338
+ - test/test_schema.rb
358
339
  - test/test_server.rb
359
340
  - test/test_side_run.rb
341
+ - test/test_store.rb
360
342
  - test/test_test.rb
361
343
  homepage: https://github.com/digital-fabric/syntropy
362
344
  licenses:
@@ -1,13 +0,0 @@
1
- export self
2
-
3
- def connection_pool
4
- @connection_pool ||= DB::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
- end
6
-
7
- def schema
8
- DB::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
- end
10
-
11
- def migrate!
12
- schema.apply(connection_pool)
13
- end
@@ -1,57 +0,0 @@
1
- *.gem
2
- *.rbc
3
- /.config
4
- /coverage/
5
- /InstalledFiles
6
- /pkg/
7
- /spec/reports/
8
- /spec/examples.txt
9
- /test/tmp/
10
- /test/version_tmp/
11
- /tmp/
12
-
13
- # Used by dotenv library to load environment variables.
14
- # .env
15
-
16
- # Ignore Byebug command history file.
17
- .byebug_history
18
-
19
- ## Specific to RubyMotion:
20
- .dat*
21
- .repl_history
22
- build/
23
- *.bridgesupport
24
- build-iPhoneOS/
25
- build-iPhoneSimulator/
26
-
27
- ## Specific to RubyMotion (use of CocoaPods):
28
- #
29
- # We recommend against adding the Pods directory to your .gitignore. However
30
- # you should judge for yourself, the pros and cons are mentioned at:
31
- # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
32
- #
33
- # vendor/Pods/
34
-
35
- ## Documentation cache and generated files:
36
- /.yardoc/
37
- /_yardoc/
38
- /doc/
39
- /rdoc/
40
-
41
- ## Environment normalization:
42
- /.bundle/
43
- /vendor/bundle
44
- /lib/bundler/man/
45
-
46
- # for a library or gem, you might want to ignore these files since the code is
47
- # intended to run in multiple environments; otherwise, check them in:
48
- # Gemfile.lock
49
- # .ruby-version
50
- # .ruby-gemset
51
-
52
- # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
53
- .rvmrc
54
-
55
- # Used by RuboCop. Remote config files pulled in from inherit_from directive.
56
- # .rubocop-https?--*
57
- log
@@ -1,32 +0,0 @@
1
- ARG RUBY_BASE_IMAGE=ruby:3.4.1-alpine
2
- ARG GEM_CACHE_IMAGE=${RUBY_BASE_IMAGE}
3
-
4
- # base image
5
- FROM ${RUBY_BASE_IMAGE} AS base
6
- RUN apk add --update sqlite-dev openssl-dev tzdata bash curl zip git
7
- RUN apk add --update build-base
8
- RUN gem install bundler:2.6.9
9
-
10
- # gem cache
11
- FROM ${GEM_CACHE_IMAGE} AS gem-cache
12
- RUN mkdir -p /usr/local/bundle
13
-
14
- FROM base AS gems
15
- COPY --from=gem-cache /usr/local/bundle /usr/local/bundle
16
- COPY Gemfile Gemfile.lock ./
17
- RUN bundle install --jobs=4 --retry=5
18
-
19
- # Final backend image
20
- FROM base AS deploy
21
-
22
- RUN adduser -D app
23
- RUN chown app:app /home/app
24
- WORKDIR /home/app
25
- USER app
26
-
27
- RUN mkdir -p /tmp
28
- COPY --from=gems --chown=app:app /usr/local/bundle /usr/local/bundle
29
-
30
- EXPOSE 1234
31
-
32
- CMD ["bundle", "exec", "syntropy", "."]
@@ -1,3 +0,0 @@
1
- source 'https://rubygems.org'
2
-
3
- gem 'syntropy', '0.3'
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
@@ -1,51 +0,0 @@
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"
@@ -1,5 +0,0 @@
1
- FROM caddy:2.10.0-builder AS builder
2
- RUN xcaddy build
3
-
4
- FROM caddy:2.10.0
5
- COPY --from=builder /usr/bin/caddy /usr/bin/caddy
@@ -1,7 +0,0 @@
1
- localhost {
2
- reverse_proxy noteflakescom-backend-1:1234
3
- }
4
-
5
- my-awesome-domain.com {
6
- reverse_proxy noteflakescom-backend-1:1234
7
- }
@@ -1,2 +0,0 @@
1
- tls {$TLS_AUTO_EMAIL}
2
-
@@ -1,4 +0,0 @@
1
- tls {
2
- dns cloudflare {env.CLOUDFLARE_API_TOKEN}
3
- }
4
-
@@ -1 +0,0 @@
1
- tls {$TLS_CUSTOM_CERT} {$TLS_CUSTOM_KEY}
@@ -1 +0,0 @@
1
- tls internal
@@ -1,11 +0,0 @@
1
- export templ { |*a, **b|
2
- html {
3
- head {
4
- title 'My awesome Syntropy website'
5
- link rel: 'stylesheet', type: 'text/css', href: '/assets/css/style.css'
6
- }
7
- body {
8
- render_yield(*a, **b)
9
- }
10
- }
11
- }
@@ -1,6 +0,0 @@
1
- ---
2
- layout: default
3
- ---
4
- # About my site
5
-
6
- Lorem ipsum my awesome site.
@@ -1,29 +0,0 @@
1
- layout = import('_layout/default')
2
-
3
- poem = [
4
- " in ten\xA0", 'M', 'inutes',
5
- ' ', 'C', 'ome back: you will',
6
- 'have taught me chi', 'N', 'ese',
7
- ' (s', 'A', 'tie).',
8
- ' shall I ret', 'U', 'rn the favor?',
9
- ' ', 'G', 'ive you',
10
- ' ot', 'H', 'er lessons',
11
- ' (', 'T', 'ing!)?',
12
- ' ', 'O', 'r would you prefer',
13
- ' sile', 'N', 'ce?',
14
- ]
15
-
16
- export Papercraft.apply(layout) {
17
- article(class: 'mesostic') {
18
- h2 'For William McN. who studied with Ezra Pound'
19
-
20
- content {
21
- span(_for: poem) { text it }
22
- }
23
-
24
- author {
25
- span "-\xA0"
26
- a 'John cage', href: 'https://en.wikipedia.org/wiki/John_Cage'
27
- }
28
- }
29
- }
@@ -1,3 +0,0 @@
1
- export templ {
2
- h1 'Articles'
3
- }
@@ -1,40 +0,0 @@
1
- body {
2
- font-family: sans-serif;
3
- }
4
-
5
- article.mesostic {
6
- width: 600px;
7
- margin: 2em auto;
8
- font-size: 1.4em;
9
-
10
- * {
11
- font-family: monospace;
12
- }
13
-
14
- h2 {
15
- text-align: center;
16
- }
17
-
18
- content {
19
- display: grid;
20
- margin: 2em 0;
21
- grid-template-columns: 1fr auto 1fr;
22
-
23
- span {
24
- display: inline-block;
25
- }
26
- span:nth-child(3n + 1) {
27
- text-align: right;
28
- }
29
-
30
- span:nth-child(3n + 2) {
31
- text-align: center;
32
- font-weight: bold;
33
- }
34
- }
35
-
36
- author {
37
- display: block;
38
- text-align: right;
39
- }
40
- }
@@ -1,15 +0,0 @@
1
- layout = import('_layout/default')
2
-
3
- export Papercraft.apply(layout) {
4
- h1 'Hello from Syntropy'
5
- p {
6
- span "Here's an "
7
- a 'about', href: 'about'
8
- span ' page.'
9
- }
10
- p {
11
- span "Here's an "
12
- a 'article', href: 'articles/cage'
13
- span ' page.'
14
- }
15
- }
@@ -1,13 +0,0 @@
1
- export self
2
-
3
- def connection_pool
4
- @connection_pool ||= DB::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
- end
6
-
7
- def schema
8
- DB::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
- end
10
-
11
- def migrate!
12
- schema.apply(connection_pool)
13
- end