syntropy 0.41.0 → 0.42.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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +10 -0
  3. data/TODO.md +334 -31
  4. data/cmd/new/template/app/_lib/storage.rb +2 -2
  5. data/cmd/new/template/config/test.rb +1 -1
  6. data/examples/agent/config/test.rb +1 -1
  7. data/examples/blog/app/_lib/posts.rb +10 -6
  8. data/examples/blog/app/_lib/storage.rb +2 -2
  9. data/examples/blog/app/posts/[id]/index.rb +2 -2
  10. data/examples/blog/config/test.rb +1 -1
  11. data/lib/syntropy/collection.rb +206 -0
  12. data/lib/syntropy/connection_pool.rb +118 -0
  13. data/lib/syntropy/controller_extensions.rb +12 -0
  14. data/lib/syntropy/http/server_connection.rb +15 -28
  15. data/lib/syntropy/json_api.rb +3 -12
  16. data/lib/syntropy/kv_store.rb +57 -0
  17. data/lib/syntropy/logger.rb +24 -47
  18. data/lib/syntropy/markdown.rb +1 -1
  19. data/lib/syntropy/module_loader.rb +53 -14
  20. data/lib/syntropy/schema.rb +95 -0
  21. data/lib/syntropy/storage.rb +42 -4
  22. data/lib/syntropy/test.rb +1 -1
  23. data/lib/syntropy/version.rb +1 -1
  24. data/lib/syntropy.rb +2 -1
  25. data/syntropy.gemspec +2 -2
  26. data/test/fixtures/app/_lib/dep_invalidate.rb +5 -0
  27. data/test/fixtures/collection/basic/_articles/2026-06-30-foo.md +6 -0
  28. data/test/fixtures/collection/basic/_articles/2026-07-13-bar.md +6 -0
  29. data/test/fixtures/collection/basic/_articles/2026-08-07-baz.md +6 -0
  30. data/test/fixtures/collection/basic/index+.rb +23 -0
  31. data/test/fixtures/collection/docs/_pages/01-foo/01-foo.md +4 -0
  32. data/test/fixtures/collection/docs/_pages/01-foo/02-bar.md +4 -0
  33. data/test/fixtures/collection/docs/_pages/01-foo/_index.yml +1 -0
  34. data/test/fixtures/collection/docs/_pages/02-bar/01-bar.md +4 -0
  35. data/test/fixtures/collection/docs/_pages/02-bar/02-baz.md +4 -0
  36. data/test/fixtures/collection/docs/_pages/02-bar/_index.yml +1 -0
  37. data/test/fixtures/collection/docs/index+.rb +16 -0
  38. data/test/test_app.rb +7 -7
  39. data/test/test_cmd_new.rb +38 -0
  40. data/test/test_collection.rb +164 -0
  41. data/test/test_connection_pool.rb +79 -3
  42. data/test/test_http_server_connection.rb +6 -6
  43. data/test/test_json_api.rb +4 -4
  44. data/test/test_kv_store.rb +6 -31
  45. data/test/test_markdown.rb +3 -3
  46. data/test/test_module_loader.rb +15 -0
  47. data/test/test_schema.rb +4 -4
  48. data/test/test_storage.rb +103 -0
  49. metadata +24 -11
  50. data/lib/syntropy/storage/connection_pool.rb +0 -106
  51. data/lib/syntropy/storage/kv_store.rb +0 -68
  52. data/lib/syntropy/storage/prepared_query.rb +0 -36
  53. data/lib/syntropy/storage/schema.rb +0 -92
  54. data/lib/syntropy/storage/store.rb +0 -33
  55. data/test/test_store.rb +0 -24
@@ -6,12 +6,13 @@ class ConnectionPoolTest < Minitest::Test
6
6
  def setup
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
- @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
9
+ @cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
10
10
 
11
11
  FileUtils.rm(@fn) rescue nil
12
12
  @standalone_db = Extralite::Database.new(@fn)
13
- @standalone_db.execute("create table foo (x,y, z)")
13
+ @standalone_db.execute("create table foo (x, y, z)")
14
14
  @standalone_db.execute("insert into foo values (1, 2, 3)")
15
+ @standalone_db.execute("insert into foo values (4, 5, 6)")
15
16
  end
16
17
 
17
18
  def teardown
@@ -26,7 +27,7 @@ class ConnectionPoolTest < Minitest::Test
26
27
  assert_kind_of Extralite::Database, db
27
28
 
28
29
  records = db.query("select * from foo")
29
- assert_equal [{x: 1, y: 2, z: 3}], records
30
+ assert_equal [{x: 1, y: 2, z: 3}, {x: 4, y: 5, z: 6}], records
30
31
  end
31
32
 
32
33
  assert_equal 1, @cp.count
@@ -52,6 +53,7 @@ class ConnectionPoolTest < Minitest::Test
52
53
  records = @standalone_db.query("select * from foo order by x")
53
54
  assert_equal [
54
55
  {x: 1, y: 2, z: 3},
56
+ {x: 4, y: 5, z: 6},
55
57
  {x: 11, y: 12, z: 13},
56
58
  {x: 21, y: 22, z: 23},
57
59
  ], records
@@ -83,4 +85,78 @@ class ConnectionPoolTest < Minitest::Test
83
85
 
84
86
  assert_equal 1, dbs.uniq.size
85
87
  end
88
+
89
+ def test_connection_pool_query
90
+ rows = @cp.query('select * from foo')
91
+ assert_equal [{x: 1, y: 2, z: 3}, {x: 4, y: 5, z: 6}], rows
92
+ end
93
+
94
+ def test_connection_pool_query_params
95
+ rows = @cp.query('select * from foo where y = ?', 5)
96
+ assert_equal [{x: 4, y: 5, z: 6}], rows
97
+
98
+ rows = @cp.query('select * from foo where y = ?', 55)
99
+ assert_equal [], rows
100
+ end
101
+
102
+ def test_connection_pool_query_transform_params
103
+ t = ->(r) { r[:z] }
104
+
105
+ rows = @cp.query(t, 'select * from foo where y <= ?', 5)
106
+ assert_equal [3, 6], rows
107
+
108
+ rows = @cp.query(t, 'select * from foo where y = ?', 55)
109
+ assert_equal [], rows
110
+ end
111
+
112
+ def test_connection_pool_query_single_row
113
+ row = @cp.query_single_row('select * from foo where x = 1')
114
+ assert_equal({x: 1, y: 2, z: 3}, row)
115
+
116
+ row = @cp.query_single_row('select * from foo where x = 3')
117
+ assert_nil row
118
+ end
119
+
120
+ def test_connection_pool_query_single_row_params
121
+ row = @cp.query_single_row('select * from foo where x = ?', 1)
122
+ assert_equal({x: 1, y: 2, z: 3}, row)
123
+
124
+ row = @cp.query_single_row('select * from foo where x = :x', x: 4)
125
+ assert_equal({x: 4, y: 5, z: 6}, row)
126
+
127
+ row = @cp.query_single_row('select * from foo where x = ?', 42)
128
+ assert_nil row
129
+ end
130
+
131
+ def test_connection_pool_query_single_row_transform_params
132
+ t = ->(r) { r[:z] }
133
+
134
+ row = @cp.query_single_row(t, 'select * from foo where x = ?', 1)
135
+ assert_equal(3, row)
136
+
137
+ row = @cp.query_single_row(t, 'select * from foo where x = :x', x: 4)
138
+ assert_equal(6, row)
139
+
140
+ row = @cp.query_single_row(t, 'select * from foo where x = ?', 42)
141
+ assert_nil row
142
+ end
143
+
144
+ def test_connection_pool_query_single_value
145
+ v = @cp.query_single_value('select y from foo where x = 1')
146
+ assert_equal 2, v
147
+
148
+ v = @cp.query_single_value('select * from foo where x = 3')
149
+ assert_nil v
150
+ end
151
+
152
+ def test_connection_pool_query_single_value_params
153
+ v = @cp.query_single_value('select y from foo where x = ?', 1)
154
+ assert_equal 2, v
155
+
156
+ v = @cp.query_single_value('select y from foo where x = :x', x: 4)
157
+ assert_equal 5, v
158
+
159
+ v = @cp.query_single_value('select * from foo where x = ?', 3)
160
+ assert_nil v
161
+ end
86
162
  end
@@ -463,7 +463,7 @@ class HTTPServerConnectionTest < Minitest::Test
463
463
  response << data
464
464
  count += 1
465
465
  @machine.snooze
466
- break if data[-7..-1] == "\r\n0\r\n\r\n"
466
+ break if data[-5..-1] == "0\r\n\r\n"
467
467
  end
468
468
 
469
469
  chunks = "#{chunk_size.to_s(16)}\r\n#{'*' * chunk_size}\r\n" * chunk_count
@@ -478,7 +478,7 @@ class HTTPServerConnectionTest < Minitest::Test
478
478
  IO.write(fn, 'foobar')
479
479
 
480
480
  @hook = ->(req) do
481
- req.respond_with_static_file(fn, nil, nil, nil)
481
+ req.respond_with_static_file(fn, nil, nil, {})
482
482
  rescue StandardError => e
483
483
  p e
484
484
  p e.backtrace
@@ -499,9 +499,9 @@ class HTTPServerConnectionTest < Minitest::Test
499
499
  response << data
500
500
  count += 1
501
501
  @machine.snooze
502
- break if data[-7..-1] == "\r\n0\r\n\r\n"
502
+ break if data[-5..-1] == "0\r\n\r\n"
503
503
  end
504
-
504
+
505
505
  content = IO.read(fn)
506
506
  file_size = content.bytesize
507
507
  expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n#{file_size.to_s(16)}\r\n#{content}\r\n0\r\n\r\n"
@@ -514,7 +514,7 @@ class HTTPServerConnectionTest < Minitest::Test
514
514
  IO.write(fn, 'foobar')
515
515
 
516
516
  @hook = ->(req) do
517
- req.respond_with_static_file(fn, nil, nil, { max_len: 3 })
517
+ req.respond_with_static_file(fn, nil, nil, {})
518
518
  req.adapter.close
519
519
  end
520
520
 
@@ -530,7 +530,7 @@ class HTTPServerConnectionTest < Minitest::Test
530
530
  @machine.snooze
531
531
  end
532
532
 
533
- expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n3\r\nfoo\r\n3\r\nbar\r\n0\r\n\r\n"
533
+ expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\n6\r\nfoobar\r\n0\r\n\r\n"
534
534
  assert_equal expected, response
535
535
  end
536
536
 
@@ -28,7 +28,7 @@ class JSONAPITest < Minitest::Test
28
28
  req = mock_req(':method' => 'GET', ':path' => '/?q=foo')
29
29
  @app.call(req)
30
30
  assert_equal HTTP::OK, req.response_status
31
- assert_equal({ 'status' => 'OK', 'response' => nil }, req.response_json)
31
+ assert_equal({ status: 'OK', response: nil }, req.response_json)
32
32
 
33
33
  req = mock_req(':method' => 'POST', ':path' => '/?q=foo')
34
34
  @app.call(req)
@@ -38,7 +38,7 @@ class JSONAPITest < Minitest::Test
38
38
  req = mock_req(':method' => 'POST', ':path' => '/?q=bar&v=foo')
39
39
  @app.call(req)
40
40
  assert_equal HTTP::OK, req.response_status
41
- assert_equal({ 'status' => 'OK', 'response' => true }, req.response_json)
41
+ assert_equal({ status: 'OK', response: true }, req.response_json)
42
42
 
43
43
  req = mock_req(':method' => 'GET', ':path' => '/?q=bar&v=foo')
44
44
  @app.call(req)
@@ -47,12 +47,12 @@ class JSONAPITest < Minitest::Test
47
47
  req = mock_req(':method' => 'GET', ':path' => '/?q=foo')
48
48
  @app.call(req)
49
49
  assert_equal HTTP::OK, req.response_status
50
- assert_equal({ 'status' => 'OK', 'response' => 'foo' }, req.response_json)
50
+ assert_equal({ status: 'OK', response: 'foo' }, req.response_json)
51
51
 
52
52
  req = mock_req(':method' => 'GET', ':path' => '/?q=foo')
53
53
  @app.call(req)
54
54
  assert_equal HTTP::OK, req.response_status
55
- assert_equal({ 'status' => 'OK', 'response' => 'foo' }, req.response_json)
55
+ assert_equal({ status: 'OK', response: 'foo' }, req.response_json)
56
56
 
57
57
  req = mock_req(':method' => 'GET', ':path' => '/?q=xxx')
58
58
  @app.call(req)
@@ -1,51 +1,27 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'helper'
4
- require 'syntropy/storage/kv_store'
5
4
 
6
5
  class KVStoreTest < Minitest::Test
7
6
  def setup
8
7
  @machine = UM.new
9
- @fn = "/tmp/#{rand(100000)}.db"
8
+ @fn = "/tmp/#{SecureRandom.hex(8)}.db"
10
9
  FileUtils.rm(@fn) rescue nil
11
- @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
10
+ @cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
12
11
  end
13
12
 
14
13
  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 }
14
+ @cp&.close
36
15
  end
37
16
 
38
17
  def test_kv_store_apply_schema
39
- assert_respond_to Syntropy::Storage::KVStore, :apply_schema
40
-
41
18
  assert_raises(Extralite::SQLError) { @cp.query('select * from kv') }
42
- Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
19
+ Syntropy::KVStore.new(@cp, 'kv')
43
20
  assert_equal [], @cp.query('select * from kv')
44
21
  end
45
22
 
46
23
  def test_kv_store_get_set
47
- Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
48
- kv_store = Syntropy::Storage::KVStore.new(@cp, 'kv')
24
+ kv_store = Syntropy::KVStore.new(@cp, 'kv')
49
25
 
50
26
  @cp.with_db do |db|
51
27
  assert_nil kv_store.get(db, 'foo')
@@ -63,8 +39,7 @@ class KVStoreTest < Minitest::Test
63
39
  end
64
40
 
65
41
  def test_kv_store_setex_sweep
66
- Syntropy::Storage::KVStore.apply_schema(@cp, 'kv')
67
- kv_store = Syntropy::Storage::KVStore.new(@cp, 'kv')
42
+ kv_store = Syntropy::KVStore.new(@cp, 'kv')
68
43
 
69
44
  @cp.with_db do |db|
70
45
  kv_store.set(db, 'foo', '123')
@@ -17,7 +17,7 @@ class MarkdownParseTest < Minitest::Test
17
17
 
18
18
  assert_equal h, atts
19
19
  assert_equal({ foo: 'bar' }, atts)
20
- assert_equal "foo *bar*\n", md2
20
+ assert_equal "foo *bar*", md2
21
21
  end
22
22
 
23
23
  def test_markdown_parse_file
@@ -38,13 +38,13 @@ class MarkdownParseTest < Minitest::Test
38
38
  atts, md = Syntropy::Markdown.parse_file(path, env)
39
39
  assert_equal "/tmp/test/#{name}", atts[:url]
40
40
  assert_equal 'baz', atts[:bar]
41
- assert_equal "bar *baz*\n", md
41
+ assert_equal "bar *baz*", md
42
42
 
43
43
  env = { app_root: '/tmp' }
44
44
  atts, md = Syntropy::Markdown.parse_file(path, env)
45
45
  assert_equal "/test/#{name}", atts[:url]
46
46
  assert_equal 'baz', atts[:bar]
47
- assert_equal "bar *baz*\n", md
47
+ assert_equal "bar *baz*", md
48
48
  end
49
49
  end
50
50
 
@@ -74,6 +74,20 @@ class ModuleLoaderTest < Minitest::Test
74
74
  assert_equal [], @loader.modules.keys
75
75
  end
76
76
 
77
+ def test_invalidate_on_file_change
78
+ _mod = @loader.load('_lib/dep_invalidate')
79
+ assert_equal ['/_lib/self', '/_lib/dep_invalidate'], @loader.modules.keys
80
+
81
+ @loader.invalidate_fn(File.join(@loader.app_root, '_lib/circular/a'))
82
+ assert_equal ['/_lib/self'], @loader.modules.keys
83
+
84
+ _mod = @loader.load('_lib/dep_invalidate')
85
+ assert_equal ['/_lib/self', '/_lib/dep_invalidate'], @loader.modules.keys
86
+
87
+ @loader.invalidate_fn(File.join(@loader.app_root, '_lib/circular/x/y/z'))
88
+ assert_equal ['/_lib/self'], @loader.modules.keys
89
+ end
90
+
77
91
  def test_index_module_env
78
92
  mod = @loader.load('mod/bar/index+')
79
93
  assert_equal '/mod/bar', mod.env[:ref]
@@ -90,6 +104,7 @@ class ModuleLoaderTest < Minitest::Test
90
104
  assert_equal [
91
105
  '_lib/callable',
92
106
  '_lib/dep',
107
+ '_lib/dep_invalidate',
93
108
  '_lib/env',
94
109
  '_lib/klass',
95
110
  '_lib/missing-export',
data/test/test_schema.rb CHANGED
@@ -7,7 +7,7 @@ class SchemaTest < Minitest::Test
7
7
  @machine = UM.new
8
8
  @fn = "/tmp/#{rand(100000)}.db"
9
9
  FileUtils.rm(@fn) rescue nil
10
- @cp = Syntropy::Storage::ConnectionPool.new(@machine, @fn, 4)
10
+ @cp = Syntropy::ConnectionPool.new(@machine, @fn, 4)
11
11
  end
12
12
 
13
13
  def teardown
@@ -15,7 +15,7 @@ class SchemaTest < Minitest::Test
15
15
  end
16
16
 
17
17
  def test_db_schema_initial
18
- schema = Syntropy::Storage::Schema.new do
18
+ schema = Syntropy::Schema.new do
19
19
  initial do |db|
20
20
  db.execute <<~SQL
21
21
  create table posts (
@@ -35,7 +35,7 @@ class SchemaTest < Minitest::Test
35
35
  end
36
36
 
37
37
  def test_db_schema_version_blocks
38
- schema = Syntropy::Storage::Schema.new do
38
+ schema = Syntropy::Schema.new do
39
39
  initial do |db|
40
40
  db.execute <<~SQL
41
41
  create table posts (
@@ -80,7 +80,7 @@ class SchemaTest < Minitest::Test
80
80
  machine: @machine,
81
81
  app_root: File.join(__dir__, 'fixtures/schema')
82
82
  })
83
- schema = Syntropy::Storage::Schema.new(module_loader:, schema_root: '/')
83
+ schema = Syntropy::Schema.new(module_loader:, schema_root: '/')
84
84
 
85
85
  assert_nil schema.current_version(@cp)
86
86
  schema.apply(@cp)
@@ -0,0 +1,103 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'securerandom'
5
+
6
+ class StorageTest < Minitest::Test
7
+ def setup
8
+ @machine = UM.new
9
+ @fn = "/tmp/#{SecureRandom.hex(8)}.db"
10
+
11
+ @module_loader = Syntropy::ModuleLoader.new({
12
+ machine: @machine,
13
+ app_root: File.join(__dir__, 'fixtures/schema')
14
+ })
15
+ end
16
+
17
+ def test_storage_missing_config
18
+ config = {}
19
+ assert_raises(Syntropy::Error) {
20
+ Syntropy::Storage.new(
21
+ @machine,
22
+ @module_loader,
23
+ config
24
+ )
25
+ }
26
+ end
27
+
28
+ def test_storage_connection_pool
29
+ config = {
30
+ path: @fn
31
+ }
32
+
33
+ storage = Syntropy::Storage.new(
34
+ @machine,
35
+ @module_loader,
36
+ config
37
+ )
38
+
39
+ cp = storage.connection_pool
40
+ assert_kind_of Syntropy::ConnectionPool, cp
41
+
42
+ db = Extralite::Database.new(@fn)
43
+
44
+ cp.with_db {
45
+ it.execute('create table x (y); insert into x (y) values (42);')
46
+ }
47
+
48
+ assert_equal 42, db.query_single_splat('select y from x')
49
+ end
50
+
51
+ def test_storage_schema
52
+ config = {
53
+ path: @fn,
54
+ schema_root: '/'
55
+ }
56
+
57
+ storage = Syntropy::Storage.new(
58
+ @machine,
59
+ @module_loader,
60
+ config
61
+ )
62
+
63
+ schema = storage.schema
64
+ assert_kind_of Syntropy::Schema, schema
65
+
66
+ cp = storage.connection_pool
67
+ assert_nil schema.current_version(cp)
68
+ schema.apply(cp)
69
+ assert_equal '2026-05-30-bar', schema.current_version(cp)
70
+
71
+ assert_equal [
72
+ {
73
+ id: 1,
74
+ title: 'foo',
75
+ body: 'baz'
76
+ }
77
+ ], cp.query('select id, title, body from posts')
78
+ end
79
+
80
+ def test_storage_query
81
+ config = { path: @fn }
82
+ storage = Syntropy::Storage.new(
83
+ @machine,
84
+ @module_loader,
85
+ config
86
+ )
87
+
88
+ r = storage.query('select :foo as a, 42 as b', foo: 'bar')
89
+ assert_equal [{ a: 'bar', b: 42 }], r
90
+ end
91
+
92
+ def test_connection_pool_prepare_splat
93
+ # pq = Syntropy::Storage.prepare_splat('select ?')
94
+ # assert_kind_of Syntropy::PreparedQuery, pq
95
+ # assert_equal 'select ?', pq.sql
96
+ # assert_equal :prepare_splat, pq.mode
97
+
98
+ # assert_kind_of Extralite::Query, @cp.with_db { it[pq] }
99
+ # assert_equal ['foo'], @cp.with_db { it[pq].bind('foo').to_a }
100
+ end
101
+
102
+
103
+ end
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.41.0
4
+ version: 0.42.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -15,14 +15,14 @@ dependencies:
15
15
  requirements:
16
16
  - - "~>"
17
17
  - !ruby/object:Gem::Version
18
- version: 3.0.1
18
+ version: 3.1.3
19
19
  type: :runtime
20
20
  prerelease: false
21
21
  version_requirements: !ruby/object:Gem::Requirement
22
22
  requirements:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
- version: 3.0.1
25
+ version: 3.1.3
26
26
  - !ruby/object:Gem::Dependency
27
27
  name: papercraft
28
28
  requirement: !ruby/object:Gem::Requirement
@@ -43,14 +43,14 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.0.4
46
+ version: 1.0.6
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 1.0.4
53
+ version: 1.0.6
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: json
56
56
  requirement: !ruby/object:Gem::Requirement
@@ -270,6 +270,8 @@ files:
270
270
  - lib/syntropy/applets/builtin/json_api.js
271
271
  - lib/syntropy/applets/builtin/ping.rb
272
272
  - lib/syntropy/applets/builtin/req.rb
273
+ - lib/syntropy/collection.rb
274
+ - lib/syntropy/connection_pool.rb
273
275
  - lib/syntropy/controller_extensions.rb
274
276
  - lib/syntropy/dev_mode.rb
275
277
  - lib/syntropy/errors.rb
@@ -281,6 +283,7 @@ files:
281
283
  - lib/syntropy/http/server_connection.rb
282
284
  - lib/syntropy/http/status.rb
283
285
  - lib/syntropy/json_api.rb
286
+ - lib/syntropy/kv_store.rb
284
287
  - lib/syntropy/logger.rb
285
288
  - lib/syntropy/markdown.rb
286
289
  - lib/syntropy/mime_types.rb
@@ -292,14 +295,10 @@ files:
292
295
  - lib/syntropy/request/response.rb
293
296
  - lib/syntropy/request/validation.rb
294
297
  - lib/syntropy/routing_tree.rb
298
+ - lib/syntropy/schema.rb
295
299
  - lib/syntropy/session.rb
296
300
  - lib/syntropy/side_run.rb
297
301
  - lib/syntropy/storage.rb
298
- - lib/syntropy/storage/connection_pool.rb
299
- - lib/syntropy/storage/kv_store.rb
300
- - lib/syntropy/storage/prepared_query.rb
301
- - lib/syntropy/storage/schema.rb
302
- - lib/syntropy/storage/store.rb
303
302
  - lib/syntropy/test.rb
304
303
  - lib/syntropy/version.rb
305
304
  - syntropy.gemspec
@@ -314,6 +313,7 @@ files:
314
313
  - test/fixtures/app/_lib/circular/b.rb
315
314
  - test/fixtures/app/_lib/circular/c.rb
316
315
  - test/fixtures/app/_lib/dep.rb
316
+ - test/fixtures/app/_lib/dep_invalidate.rb
317
317
  - test/fixtures/app/_lib/env.rb
318
318
  - test/fixtures/app/_lib/klass.rb
319
319
  - test/fixtures/app/_lib/missing-export.rb
@@ -366,6 +366,17 @@ files:
366
366
  - test/fixtures/app_setup/index.rb
367
367
  - test/fixtures/app_with_schema/_schema/2026-01-02-foo.rb
368
368
  - test/fixtures/app_with_schema/_schema/2026-05-30-bar.rb
369
+ - test/fixtures/collection/basic/_articles/2026-06-30-foo.md
370
+ - test/fixtures/collection/basic/_articles/2026-07-13-bar.md
371
+ - test/fixtures/collection/basic/_articles/2026-08-07-baz.md
372
+ - test/fixtures/collection/basic/index+.rb
373
+ - test/fixtures/collection/docs/_pages/01-foo/01-foo.md
374
+ - test/fixtures/collection/docs/_pages/01-foo/02-bar.md
375
+ - test/fixtures/collection/docs/_pages/01-foo/_index.yml
376
+ - test/fixtures/collection/docs/_pages/02-bar/01-bar.md
377
+ - test/fixtures/collection/docs/_pages/02-bar/02-baz.md
378
+ - test/fixtures/collection/docs/_pages/02-bar/_index.yml
379
+ - test/fixtures/collection/docs/index+.rb
369
380
  - test/fixtures/controllers/by_host/bar.com/index.rb
370
381
  - test/fixtures/controllers/by_host/foo.com/index.rb
371
382
  - test/fixtures/controllers/by_host_dir.rb
@@ -379,6 +390,8 @@ files:
379
390
  - test/run.rb
380
391
  - test/test_app.rb
381
392
  - test/test_caching.rb
393
+ - test/test_cmd_new.rb
394
+ - test/test_collection.rb
382
395
  - test/test_connection_pool.rb
383
396
  - test/test_controller.rb
384
397
  - test/test_errors.rb
@@ -398,7 +411,7 @@ files:
398
411
  - test/test_schema.rb
399
412
  - test/test_server.rb
400
413
  - test/test_side_run.rb
401
- - test/test_store.rb
414
+ - test/test_storage.rb
402
415
  - test/test_test.rb
403
416
  homepage: https://github.com/digital-fabric/syntropy
404
417
  licenses:
@@ -1,106 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'extralite'
4
-
5
- module Syntropy
6
- module Storage
7
- # ConnectionPool implements concurrent access to an SQLite database.
8
- class ConnectionPool
9
- attr_reader :count
10
-
11
- # Initializes the connection pool.
12
- #
13
- # @param machine [UringMachine] machine instance
14
- # @param fn [String] database filename
15
- # @param max_conn [Integer] maximum number of connections
16
- # @return [void]
17
- def initialize(machine, fn, max_conn)
18
- @machine = machine
19
- @fn = fn
20
- @count = 0
21
- @max_conn = max_conn
22
- @queue = UM::Queue.new
23
- @key = :"connection_pool_#{object_id}"
24
- end
25
-
26
- # Checks out a database connection, passing it to the given block. This
27
- # method is reentrant - called from the same fiber, it will yield the same
28
- # database connection.
29
- #
30
- # @return [Extralite::Database] database connection
31
- def with_db
32
- if (db = Thread.current[@key])
33
- @machine.snooze
34
- return yield(db)
35
- end
36
-
37
- db = checkout
38
- begin
39
- Thread.current[@key] = db
40
- yield(db)
41
- ensure
42
- Thread.current[@key] = nil
43
- checkin(db)
44
- end
45
- end
46
-
47
- # Performs the given query by acquiring a database connection and running
48
- # the query.
49
- #
50
- # @param sql [String] SQL query
51
- # @return [Array<Hash>] result rows
52
- def query(sql, *, **, &)
53
- with_db { it.query(sql, *, **, &) }
54
- end
55
-
56
- # Executes the given query by acquiring a database connection and running
57
- # the query.
58
- #
59
- # @param sql [String] SQL query
60
- # @return [Integer] number of changed rows
61
- def execute(sql, *, **)
62
- with_db { it.execute(sql, *, **) }
63
- end
64
-
65
- # Closes the connection pool by removing all connections.
66
- #
67
- # @return [void]
68
- def close
69
- while @queue.count > 0
70
- db = @machine.shift(@queue)
71
- db.close
72
- @count -= 1
73
- end
74
- end
75
-
76
- private
77
-
78
- # Checks out a connection from the pool.
79
- #
80
- # @return [Extralite::Database] database connection
81
- def checkout
82
- return make_db_instance if @queue.count == 0 && @count < @max_conn
83
-
84
- @machine.shift(@queue)
85
- end
86
-
87
- # Checks in a connection to the pool.
88
- #
89
- # @param db [Extralite::Database] database connection
90
- # @return [void]
91
- def checkin(db)
92
- @machine.push(@queue, db)
93
- end
94
-
95
- # Creates a database connection.
96
- #
97
- # @return [Extralite::Database] database connection
98
- def make_db_instance
99
- Extralite::Database.new(@fn, wal: true).tap do
100
- @count += 1
101
- it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
102
- end
103
- end
104
- end
105
- end
106
- end