syntropy 0.34.4 → 0.36.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/TODO.md +0 -2
  4. data/bin/syntropy +1 -1
  5. data/cmd/console.rb +1 -1
  6. data/{examples → cmd/new}/template/.gitignore +1 -0
  7. data/cmd/new/template/Dockerfile +25 -0
  8. data/{examples → cmd/new}/template/Gemfile +1 -1
  9. data/cmd/new/template/README.md +40 -0
  10. data/cmd/new/template/app/_lib/storage.rb +13 -0
  11. data/cmd/new/template/docker-compose.yml +46 -0
  12. data/cmd/new.rb +46 -0
  13. data/examples/blog/app/_lib/posts.rb +3 -3
  14. data/examples/blog/app/_lib/storage.rb +13 -0
  15. data/lib/syntropy/module_loader.rb +5 -1
  16. data/lib/syntropy/{db → storage}/connection_pool.rb +2 -1
  17. data/lib/syntropy/storage/kv_store.rb +68 -0
  18. data/lib/syntropy/storage/prepared_query.rb +36 -0
  19. data/lib/syntropy/{db → storage}/schema.rb +1 -1
  20. data/lib/syntropy/{db → storage}/store.rb +1 -1
  21. data/lib/syntropy/storage.rb +6 -0
  22. data/lib/syntropy/test.rb +3 -1
  23. data/lib/syntropy/version.rb +1 -1
  24. data/lib/syntropy.rb +2 -4
  25. data/test/{test_db_connection_pool.rb → test_connection_pool.rb} +4 -4
  26. data/test/test_kv_store.rb +84 -0
  27. data/test/{test_db_schema.rb → test_schema.rb} +5 -5
  28. data/test/{test_db_store.rb → test_store.rb} +3 -3
  29. metadata +28 -43
  30. data/cmd/setup/template/site/.gitignore +0 -57
  31. data/cmd/setup/template/site/Dockerfile +0 -32
  32. data/cmd/setup/template/site/Gemfile +0 -3
  33. data/cmd/setup/template/site/README.md +0 -0
  34. data/cmd/setup/template/site/bin/console +0 -0
  35. data/cmd/setup/template/site/bin/restart +0 -0
  36. data/cmd/setup/template/site/bin/server +0 -0
  37. data/cmd/setup/template/site/bin/start +0 -0
  38. data/cmd/setup/template/site/bin/stop +0 -0
  39. data/cmd/setup/template/site/docker-compose.yml +0 -51
  40. data/cmd/setup/template/site/proxy/Dockerfile +0 -5
  41. data/cmd/setup/template/site/proxy/etc/Caddyfile +0 -7
  42. data/cmd/setup/template/site/proxy/etc/tls_auto +0 -2
  43. data/cmd/setup/template/site/proxy/etc/tls_cloudflare +0 -4
  44. data/cmd/setup/template/site/proxy/etc/tls_custom +0 -1
  45. data/cmd/setup/template/site/proxy/etc/tls_selfsigned +0 -1
  46. data/cmd/setup/template/site/site/_layout/default.rb +0 -11
  47. data/cmd/setup/template/site/site/about.md +0 -6
  48. data/cmd/setup/template/site/site/articles/cage.rb +0 -29
  49. data/cmd/setup/template/site/site/articles/index.rb +0 -3
  50. data/cmd/setup/template/site/site/assets/css/style.css +0 -40
  51. data/cmd/setup/template/site/site/assets/img/syntropy.png +0 -0
  52. data/cmd/setup/template/site/site/index.rb +0 -15
  53. data/examples/blog/app/_lib/database.rb +0 -13
  54. data/examples/template/app/_lib/database.rb +0 -13
  55. /data/{examples → cmd/new}/template/app/_layout/default.rb +0 -0
  56. /data/{examples → cmd/new}/template/app/_schema/2026-01-01-initial.rb +0 -0
  57. /data/{examples → cmd/new}/template/app/assets/style.css +0 -0
  58. /data/{examples → cmd/new}/template/app/index.rb +0 -0
  59. /data/{examples → cmd/new}/template/app/test.rb +0 -0
  60. /data/{examples → cmd/new}/template/config/development.rb +0 -0
  61. /data/{examples → cmd/new}/template/config/production.rb +0 -0
  62. /data/{examples → cmd/new}/template/config/test.rb +0 -0
  63. /data/{examples → cmd/new}/template/test/test_app.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6db3fba37a6c6dbc673424d2f68a83164b5a802e569862cfc6757aa128b454a2
4
- data.tar.gz: c447710a87fd8c9833e6a677488c6be8cdc441fa5f0e5aa1729c223316e0af54
3
+ metadata.gz: f92ed1375642f12fb92654d872bfde1d07c7ad74be97507af3c1616390b6ce14
4
+ data.tar.gz: 5072d514bc07149f1d26bc900bcc9379970ebbbaf1b88eaa23933189fb684b37
5
5
  SHA512:
6
- metadata.gz: 3858b134e15456c71cfd2639c588c98014837aa5a695f1c32a5f1bd4438d4da3a3c82fd9a4e9955396e6827e3e6d49f95ec5116b3ccce77cf856173b7ef0c746
7
- data.tar.gz: 86297d5dc498b1130f66039e3792ea06bc51848f66002c231af13b38e2efa6c2f3f274ac3a22c9b3f8172a9e72c6b1f3ebabcb7fb296c2eb6792bee03659a98d
6
+ metadata.gz: f77aed02801a7b1c10f67a5ed7fbac9196580bd482c19ca660c81beaf5f2adcba86047635e35f9750d003538b12401962888193c058fbc3023ae88eae9038506
7
+ data.tar.gz: f923ab3aa4e535d5a85a563acf7f97534639ca1ed3d02e2c2b94d9cecef57e06cefc3a72a1ca782d0118b012189298bc3c743b017c0ee2d06be8bbf55d03dc89
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ # 0.36.0 2026-06-04
2
+
3
+ - Rename `DB` to `Storage`
4
+ - Add `PreparedQuery`, `KVStore`
5
+
6
+ # 0.35.0 2026-06-04
7
+
8
+ - Add `syntropy new` command
9
+ - Finalize work on new app template
10
+
1
11
  # 0.34.4 2026-06-03
2
12
 
3
13
  - Really fix running test in watch mode with docker compose
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
data/bin/syntropy CHANGED
@@ -11,5 +11,5 @@ cmd = 'help' if cmd !~ /^[a-z]+$/
11
11
  fn = cmd_fn(cmd)
12
12
  fn = cmd_fn('help') if !File.file?(fn)
13
13
 
14
- puts SYNTROPY_BANNER
14
+ # puts SYNTROPY_BANNER
15
15
  require(fn)
data/cmd/console.rb CHANGED
@@ -15,7 +15,7 @@ env = {
15
15
  }
16
16
 
17
17
  parser = OptionParser.new do |o|
18
- o.banner = 'Usage: syntropy serve [options]'
18
+ o.banner = 'Usage: syntropy console [options]'
19
19
 
20
20
  o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
21
21
  env[:app_root] = path
@@ -1,2 +1,3 @@
1
1
  Gemfile.lock
2
2
  storage/*
3
+ vendor/bundle
@@ -0,0 +1,25 @@
1
+ FROM ruby:4.0-slim AS base
2
+ WORKDIR /syntropy
3
+ RUN apt-get update -qq && apt-get install -y \
4
+ curl \
5
+ build-essential \
6
+ libpq-dev \
7
+ libsqlite3-dev \
8
+ libssl-dev \
9
+ libyaml-dev \
10
+ pkg-config && \
11
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
12
+
13
+ ENV BUNDLE_PATH="/usr/local/bundle"
14
+ COPY Gemfile Gemfile.lock ./
15
+ RUN bundle install
16
+
17
+ RUN groupadd --system --gid 1000 syntropy && \
18
+ useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
19
+ USER 1000:1000
20
+
21
+ EXPOSE 1234
22
+
23
+ # Start the main process
24
+ # CMD ["bash"]
25
+ CMD ["bundle", "exec", "syntropy", "serve"]
@@ -1,3 +1,3 @@
1
1
  source 'https://gem.coop'
2
2
 
3
- gem 'syntropy', '~>0.34.3'
3
+ gem 'syntropy', '~>0.36.0'
@@ -0,0 +1,40 @@
1
+ # My awesome Syntropy app
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ $ bundle install
7
+ ```
8
+
9
+ ## Running the web server
10
+
11
+ ```bash
12
+ $ bundle exec syntropy serve
13
+ ```
14
+
15
+ ## Starting the app console
16
+
17
+ ```bash
18
+ $ bundle exec syntropy console
19
+ ```
20
+
21
+ ### Running tests
22
+
23
+ ```bash
24
+ $ bundle exec syntropy tests
25
+ ```
26
+
27
+ ### Usage with Docker Compose
28
+
29
+ You can also run the app on Docker Compose:
30
+
31
+ ```bash
32
+ # run the app server
33
+ $ docker compose up
34
+
35
+ # run the console
36
+ $ docker compose run --remove-orphans console
37
+
38
+ # run tests
39
+ $ docker compose run --remove-orphans test
40
+ ```
@@ -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,46 @@
1
+ #version: "3.8"
2
+
3
+ services:
4
+ app_server:
5
+ build: .
6
+ security_opt:
7
+ - seccomp:unconfined
8
+ volumes:
9
+ - .:/syntropy
10
+ ports:
11
+ - "1234:1234"
12
+ stop_signal: SIGINT
13
+ stop_grace_period: 10s
14
+ restart: always
15
+ healthcheck:
16
+ test: "curl 'http://localhost:1234/'"
17
+ interval: "30s"
18
+ timeout: "3s"
19
+ start_period: "5s"
20
+ retries: 3
21
+ console:
22
+ build: .
23
+ command: bundle exec syntropy console
24
+ stdin_open: true # docker run -i
25
+ tty: true
26
+ security_opt:
27
+ - seccomp:unconfined
28
+ profiles:
29
+ - dev
30
+ volumes:
31
+ - .:/syntropy
32
+ stop_signal: SIGINT
33
+ restart: never
34
+ test:
35
+ build: .
36
+ command: bundle exec syntropy test -w
37
+ stdin_open: true # docker run -i
38
+ tty: true
39
+ security_opt:
40
+ - seccomp:unconfined
41
+ profiles:
42
+ - dev
43
+ volumes:
44
+ - .:/syntropy
45
+ stop_signal: SIGINT
46
+ restart: never
data/cmd/new.rb ADDED
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'fileutils'
5
+
6
+ parser = OptionParser.new do |o|
7
+ o.banner = 'Usage: syntropy new NAME [options]'
8
+
9
+ o.on('-h', '--help', 'Show this help message') do
10
+ puts o
11
+ exit
12
+ end
13
+ end
14
+
15
+ begin
16
+ parser.parse!
17
+ rescue OptionParser::InvalidOption
18
+ puts parser
19
+ exit
20
+ rescue StandardError => e
21
+ p e
22
+ puts e.message
23
+ puts e.backtrace.join("\n")
24
+ exit
25
+ end
26
+
27
+ path = ARGV.shift
28
+ if !path
29
+ $stdout << 'Please enter a name for your app: '
30
+ path = $stdin.gets
31
+ end
32
+
33
+ full_path = File.expand_path(path)
34
+ puts "Creating app in #{full_path}"
35
+
36
+ template_path = File.join(__dir__, 'new/template')
37
+
38
+ begin
39
+ `mkdir -p "#{path}"`
40
+ `cp -r #{template_path}/* "#{path}/"`
41
+ puts "Your app is ready in #{path}"
42
+ rescue => e
43
+ p e
44
+ p e.backtrace
45
+ exit(1)
46
+ end
@@ -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
@@ -199,7 +199,8 @@ module Syntropy
199
199
  env[:logger]&.info(message: "Loaded module at #{fn}")
200
200
  m
201
201
  rescue SyntaxError => e
202
- STDERR.puts("\n#{e.message}")
202
+ env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
203
+ STDERR.puts("\n#{e.message}") if !Syntropy.test_mode
203
204
 
204
205
  if (m = e.message.match(/^(.+)\: syntax/))
205
206
  location = m[1]
@@ -209,6 +210,9 @@ module Syntropy
209
210
  else
210
211
  raise e
211
212
  end
213
+ rescue => e
214
+ env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
215
+ raise e
212
216
  end
213
217
 
214
218
  # Initializes a module with the given environment hash.
@@ -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 = {}
@@ -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,7 +118,7 @@ module Syntropy
118
118
  )
119
119
  @test_harness = Syntropy::TestHarness.new(@app)
120
120
 
121
- @db = load_module('/_lib/database', raise_on_missing: false)
121
+ @db = load_module('/_lib/storage', raise_on_missing: false)
122
122
  @db&.migrate!
123
123
  end
124
124
 
@@ -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.34.4'
4
+ VERSION = '0.36.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).
@@ -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]
@@ -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
@@ -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.34.4
4
+ version: 0.36.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -170,30 +170,23 @@ files:
170
170
  - cmd/_banner.rb
171
171
  - cmd/console.rb
172
172
  - cmd/help.rb
173
+ - cmd/new.rb
174
+ - cmd/new/template/.gitignore
175
+ - cmd/new/template/Dockerfile
176
+ - cmd/new/template/Gemfile
177
+ - cmd/new/template/README.md
178
+ - cmd/new/template/app/_layout/default.rb
179
+ - cmd/new/template/app/_lib/storage.rb
180
+ - cmd/new/template/app/_schema/2026-01-01-initial.rb
181
+ - cmd/new/template/app/assets/style.css
182
+ - cmd/new/template/app/index.rb
183
+ - cmd/new/template/app/test.rb
184
+ - cmd/new/template/config/development.rb
185
+ - cmd/new/template/config/production.rb
186
+ - cmd/new/template/config/test.rb
187
+ - cmd/new/template/docker-compose.yml
188
+ - cmd/new/template/test/test_app.rb
173
189
  - cmd/serve.rb
174
- - cmd/setup/template/site/.gitignore
175
- - cmd/setup/template/site/Dockerfile
176
- - cmd/setup/template/site/Gemfile
177
- - cmd/setup/template/site/README.md
178
- - cmd/setup/template/site/bin/console
179
- - cmd/setup/template/site/bin/restart
180
- - cmd/setup/template/site/bin/server
181
- - cmd/setup/template/site/bin/start
182
- - cmd/setup/template/site/bin/stop
183
- - cmd/setup/template/site/docker-compose.yml
184
- - cmd/setup/template/site/proxy/Dockerfile
185
- - cmd/setup/template/site/proxy/etc/Caddyfile
186
- - cmd/setup/template/site/proxy/etc/tls_auto
187
- - cmd/setup/template/site/proxy/etc/tls_cloudflare
188
- - cmd/setup/template/site/proxy/etc/tls_custom
189
- - cmd/setup/template/site/proxy/etc/tls_selfsigned
190
- - cmd/setup/template/site/site/_layout/default.rb
191
- - cmd/setup/template/site/site/about.md
192
- - cmd/setup/template/site/site/articles/cage.rb
193
- - cmd/setup/template/site/site/articles/index.rb
194
- - cmd/setup/template/site/site/assets/css/style.css
195
- - cmd/setup/template/site/site/assets/img/syntropy.png
196
- - cmd/setup/template/site/site/index.rb
197
190
  - cmd/test.rb
198
191
  - docker-compose.yml
199
192
  - examples/basic/bad.rb
@@ -206,8 +199,8 @@ files:
206
199
  - examples/basic/templates.rb
207
200
  - examples/blog/.gitignore
208
201
  - examples/blog/app/_layout/default.rb
209
- - examples/blog/app/_lib/database.rb
210
202
  - examples/blog/app/_lib/posts.rb
203
+ - examples/blog/app/_lib/storage.rb
211
204
  - examples/blog/app/_schema/2026-01-01-initial.rb
212
205
  - examples/blog/app/assets/style.css
213
206
  - examples/blog/app/index.rb
@@ -235,18 +228,6 @@ files:
235
228
  - examples/mcp-oauth/app/signin.rb
236
229
  - examples/mcp-oauth/test/test_app.rb
237
230
  - examples/mcp-oauth/test/test_oauth.rb
238
- - examples/template/.gitignore
239
- - examples/template/Gemfile
240
- - examples/template/app/_layout/default.rb
241
- - examples/template/app/_lib/database.rb
242
- - examples/template/app/_schema/2026-01-01-initial.rb
243
- - examples/template/app/assets/style.css
244
- - examples/template/app/index.rb
245
- - examples/template/app/test.rb
246
- - examples/template/config/development.rb
247
- - examples/template/config/production.rb
248
- - examples/template/config/test.rb
249
- - examples/template/test/test_app.rb
250
231
  - lib/syntropy.rb
251
232
  - lib/syntropy/app.rb
252
233
  - lib/syntropy/applets/builtin/auto_refresh/watch.js
@@ -258,9 +239,6 @@ files:
258
239
  - lib/syntropy/applets/builtin/json_api.js
259
240
  - lib/syntropy/applets/builtin/ping.rb
260
241
  - lib/syntropy/applets/builtin/req.rb
261
- - lib/syntropy/db/connection_pool.rb
262
- - lib/syntropy/db/schema.rb
263
- - lib/syntropy/db/store.rb
264
242
  - lib/syntropy/dev_mode.rb
265
243
  - lib/syntropy/errors.rb
266
244
  - lib/syntropy/http.rb
@@ -284,6 +262,12 @@ files:
284
262
  - lib/syntropy/routing_tree.rb
285
263
  - lib/syntropy/session.rb
286
264
  - lib/syntropy/side_run.rb
265
+ - lib/syntropy/storage.rb
266
+ - lib/syntropy/storage/connection_pool.rb
267
+ - lib/syntropy/storage/kv_store.rb
268
+ - lib/syntropy/storage/prepared_query.rb
269
+ - lib/syntropy/storage/schema.rb
270
+ - lib/syntropy/storage/store.rb
287
271
  - lib/syntropy/test.rb
288
272
  - lib/syntropy/utils.rb
289
273
  - lib/syntropy/version.rb
@@ -336,23 +320,24 @@ files:
336
320
  - test/run.rb
337
321
  - test/test_app.rb
338
322
  - test/test_caching.rb
339
- - test/test_db_connection_pool.rb
340
- - test/test_db_schema.rb
341
- - test/test_db_store.rb
323
+ - test/test_connection_pool.rb
342
324
  - test/test_errors.rb
343
325
  - test/test_http_client.rb
344
326
  - test/test_http_client_connection.rb
345
327
  - test/test_http_protocol.rb
346
328
  - test/test_http_server_connection.rb
347
329
  - test/test_json_api.rb
330
+ - test/test_kv_store.rb
348
331
  - test/test_mock_adapter.rb
349
332
  - test/test_module_loader.rb
350
333
  - test/test_request.rb
351
334
  - test/test_request_session.rb
352
335
  - test/test_response.rb
353
336
  - test/test_routing_tree.rb
337
+ - test/test_schema.rb
354
338
  - test/test_server.rb
355
339
  - test/test_side_run.rb
340
+ - test/test_store.rb
356
341
  - test/test_test.rb
357
342
  homepage: https://github.com/digital-fabric/syntropy
358
343
  licenses:
@@ -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
- }
File without changes
@@ -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
@@ -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
File without changes
File without changes
File without changes
File without changes