syntropy 0.31.0 → 0.33.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/.gitignore +2 -0
  3. data/CHANGELOG.md +20 -0
  4. data/TODO.md +7 -1
  5. data/cmd/console.rb +77 -0
  6. data/cmd/serve.rb +1 -3
  7. data/cmd/test.rb +76 -20
  8. data/examples/blog/app/_layout/default.rb +11 -0
  9. data/examples/blog/app/_lib/post_store.rb +47 -0
  10. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  11. data/examples/blog/app/_setup.rb +4 -0
  12. data/examples/blog/app/index.rb +7 -0
  13. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  14. data/examples/blog/app/posts/[id]/index.rb +61 -0
  15. data/examples/blog/app/posts/index.rb +40 -0
  16. data/examples/blog/app/posts/new.rb +29 -0
  17. data/examples/mcp-oauth/README.md +3 -3
  18. data/examples/mcp-oauth/app/mcp.rb +55 -8
  19. data/examples/mcp-oauth/app/oauth/authorize.rb +0 -8
  20. data/examples/mcp-oauth/app/oauth/register.rb +0 -1
  21. data/examples/mcp-oauth/test/test_app.rb +2 -20
  22. data/examples/mcp-oauth/test/test_oauth.rb +93 -217
  23. data/lib/syntropy/app.rb +23 -9
  24. data/lib/syntropy/db/connection_pool.rb +71 -0
  25. data/lib/syntropy/db/schema.rb +92 -0
  26. data/lib/syntropy/db/store.rb +31 -0
  27. data/lib/syntropy/http/io_extensions.rb +33 -5
  28. data/lib/syntropy/http/server_connection.rb +21 -62
  29. data/lib/syntropy/{module.rb → module_loader.rb} +48 -8
  30. data/lib/syntropy/request/request_info.rb +3 -4
  31. data/lib/syntropy/request/response.rb +2 -2
  32. data/lib/syntropy/request/session.rb +113 -0
  33. data/lib/syntropy/request/validation.rb +1 -2
  34. data/lib/syntropy/request.rb +9 -0
  35. data/lib/syntropy/test.rb +84 -1
  36. data/lib/syntropy/version.rb +1 -1
  37. data/lib/syntropy.rb +4 -2
  38. data/syntropy.gemspec +3 -1
  39. data/test/app/_hook.rb +1 -1
  40. data/test/app/by_method.rb +9 -0
  41. data/test/app_setup/_setup.rb +7 -0
  42. data/test/app_setup/index.rb +1 -0
  43. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  44. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  45. data/test/schema/2026-01-02-foo.rb +12 -0
  46. data/test/schema/2026-05-30-bar.rb +7 -0
  47. data/test/test_app.rb +58 -3
  48. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  49. data/test/test_db_schema.rb +96 -0
  50. data/test/test_db_store.rb +24 -0
  51. data/test/test_http_protocol.rb +250 -0
  52. data/test/test_http_server_connection.rb +18 -24
  53. data/test/test_json_api.rb +1 -1
  54. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  55. data/test/test_request.rb +7 -4
  56. data/test/test_request_session.rb +254 -0
  57. data/test/test_server.rb +9 -13
  58. metadata +63 -12
  59. data/examples/mcp-oauth/test/helper.rb +0 -9
  60. data/lib/syntropy/connection_pool.rb +0 -61
@@ -1,26 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
4
-
5
- class AppTest < Minitest::Test
6
- APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
7
- HTTP = Syntropy::HTTP
8
-
9
- def setup
10
- @machine = UM.new
11
- @app = Syntropy::App.new(
12
- root_dir: APP_ROOT,
13
- mount_path: '/',
14
- machine: @machine
15
- )
16
- @test_harness = Syntropy::TestHarness.new(@app)
17
- end
18
-
3
+ class AppTest < Syntropy::Test
19
4
  def test_root
20
- req = @test_harness.request(
21
- ':method' => 'GET',
22
- ':path' => '/'
23
- )
5
+ req = get('/')
24
6
  assert_equal HTTP::OK, req.response_status
25
7
  assert_match /Syntropy/, req.response_body
26
8
  end
@@ -1,28 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
4
3
  require 'base64'
5
4
  require 'digest'
6
5
 
7
- class OAuthBaseTest < Minitest::Test
8
- APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
9
- HTTP = Syntropy::HTTP
10
-
6
+ class OAuthBaseTest < Syntropy::Test
11
7
  def setup
12
- @machine = UM.new
13
- @app = Syntropy::App.new(
14
- root_dir: APP_ROOT,
15
- mount_path: '/',
16
- machine: @machine
17
- )
18
- @store = @app.module_loader.load('_lib/auth_store')
19
- @test_harness = Syntropy::TestHarness.new(@app)
20
- end
21
-
22
- def teardown
23
- @machine = nil
24
- @app = nil
25
- @test_harness = nil
8
+ super
9
+ @store = load_module('_lib/auth_store')
26
10
  end
27
11
  end
28
12
 
@@ -42,18 +26,15 @@ end
42
26
 
43
27
  class OAuthPhase1DiscoveryTest < OAuthBaseTest
44
28
  def test_mcp_no_bearer_token
45
- req = @test_harness.request(
29
+ req = post_json(
30
+ '/mcp',
46
31
  {
47
- ':method' => 'POST',
48
- ':path' => '/mcp',
49
- 'content-type' => 'application/json'
50
- },
51
- JSON.dump({
52
32
  method: 'initialize',
53
33
  jsonrpc: '2.0',
54
34
  params: {}
55
- })
35
+ }
56
36
  )
37
+
57
38
  assert_equal HTTP::UNAUTHORIZED, req.response_status
58
39
 
59
40
  www_auth = req.response_headers['WWW-Authenticate']
@@ -62,18 +43,14 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
62
43
  end
63
44
 
64
45
  def test_mcp_invalid_bearer_token
65
- req = @test_harness.request(
46
+ req = post_json(
47
+ '/mcp',
66
48
  {
67
- ':method' => 'POST',
68
- ':path' => '/mcp',
69
- 'content-type' => 'application/json',
70
- 'authorization' => 'Bearer foobar'
71
- },
72
- JSON.dump({
73
49
  method: 'initialize',
74
50
  jsonrpc: '2.0',
75
51
  params: {}
76
- })
52
+ },
53
+ 'authorization' => 'Bearer foobar'
77
54
  )
78
55
  assert_equal HTTP::UNAUTHORIZED, req.response_status
79
56
 
@@ -83,10 +60,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
83
60
  end
84
61
 
85
62
  def test_oauth_protected_resource_metadatas
86
- req = @test_harness.request(
87
- ':method' => 'GET',
88
- ':path' => '/.well-known/oauth-protected-resource'
89
- )
63
+ req = get('/.well-known/oauth-protected-resource')
90
64
  assert_equal HTTP::OK, req.response_status
91
65
  json = req.response_json
92
66
  assert_equal ["http://localhost:1234/"], json['authorization_servers']
@@ -94,10 +68,7 @@ class OAuthPhase1DiscoveryTest < OAuthBaseTest
94
68
  end
95
69
 
96
70
  def test_oauth_authorization_server_metadata
97
- req = @test_harness.request(
98
- ':method' => 'GET',
99
- ':path' => '/.well-known/oauth-authorization-server'
100
- )
71
+ req = get('/.well-known/oauth-authorization-server')
101
72
  assert_equal HTTP::OK, req.response_status
102
73
  json = req.response_json
103
74
  assert_equal "http://localhost:1234/", json['issuer']
@@ -117,14 +88,10 @@ class OAuthPhase2ClientRegistrationTest < OAuthBaseTest
117
88
  "grant_types" => ["authorization_code", "refresh_token"]
118
89
  }
119
90
 
120
- req = @test_harness.request(
121
- {
122
- ':method' => 'POST',
123
- ':path' => '/oauth/register',
124
- 'content-type' => 'application/json',
125
- 'authorization' => 'Bearer foobar'
126
- },
127
- JSON.dump(client_info)
91
+ req = post_json(
92
+ '/oauth/register',
93
+ client_info,
94
+ 'authorization' => 'Bearer foobar'
128
95
  )
129
96
 
130
97
  assert_equal HTTP::CREATED, req.response_status
@@ -148,14 +115,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
148
115
  "redirect_uris" => ["http://localhost:8400/callback"],
149
116
  "grant_types" => ["authorization_code", "refresh_token"]
150
117
  }
151
- req = @test_harness.request(
152
- {
153
- ':method' => 'POST',
154
- ':path' => '/oauth/register',
155
- 'content-type' => 'application/json',
156
- 'authorization' => 'Bearer foobar'
157
- },
158
- JSON.dump(client_info)
118
+ req = post_json(
119
+ '/oauth/register',
120
+ client_info,
121
+ 'authorization' => 'Bearer foobar'
159
122
  )
160
123
  assert_equal HTTP::CREATED, req.response_status
161
124
  json = req.response_json
@@ -169,10 +132,7 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
169
132
  'code_challenge_method' => 'S256',
170
133
  'state' => 'my_state'
171
134
  }
172
- req = @test_harness.request(
173
- ':method' => 'GET',
174
- ':path' => "/oauth/authorize?#{URI.encode_www_form(params)}"
175
- )
135
+ req = get("/oauth/authorize?#{URI.encode_www_form(params)}")
176
136
  assert_equal HTTP::FOUND, req.response_status
177
137
  assert_equal '/signin', req.response_headers['Location']
178
138
 
@@ -202,17 +162,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
202
162
  }
203
163
  oauth_signin_id = @store.store(auth_params)
204
164
 
205
- req = @test_harness.request(
206
- {
207
- ':method' => 'POST',
208
- ':path' => '/signin',
209
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
210
- 'content-type' => 'application/x-www-form-urlencoded'
211
- },
212
- URI.encode_www_form(
213
- username: 'foobar',
214
- password: 'foobar'
215
- )
165
+ req = post_form(
166
+ '/signin',
167
+ { username: 'foobar', password: 'foobar' },
168
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
216
169
  )
217
170
 
218
171
  auth_info = @store.fetch(oauth_signin_id)
@@ -227,27 +180,15 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
227
180
  end
228
181
 
229
182
  def test_signin_endpoint_get
230
- req = @test_harness.request(
231
- {
232
- ':method' => 'GET',
233
- ':path' => '/signin',
234
- }
235
- )
183
+ req = get('/signin')
236
184
  assert_equal HTTP::OK, req.response_status
237
185
  assert_equal 'text/html', req.response_content_type
238
186
  end
239
187
 
240
188
  def test_signin_endpoint_post_bad_creds
241
- req = @test_harness.request(
242
- {
243
- ':method' => 'POST',
244
- ':path' => '/signin',
245
- 'content-type' => 'application/x-www-form-urlencoded'
246
- },
247
- URI.encode_www_form(
248
- username: 'foobar',
249
- password: 'bad'
250
- )
189
+ req = post_form(
190
+ '/signin',
191
+ { username: 'foobar', password: 'bad' }
251
192
  )
252
193
  assert_equal HTTP::UNAUTHORIZED, req.response_status
253
194
  assert_equal 'text/html', req.response_content_type
@@ -255,16 +196,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
255
196
  end
256
197
 
257
198
  def test_signin_endpoint_post_good_creds
258
- req = @test_harness.request(
259
- {
260
- ':method' => 'POST',
261
- ':path' => '/signin',
262
- 'content-type' => 'application/x-www-form-urlencoded'
263
- },
264
- URI.encode_www_form(
265
- username: 'foobar',
266
- password: 'foobar'
267
- )
199
+ req = post_form(
200
+ '/signin',
201
+ { username: 'foobar', password: 'foobar' }
268
202
  )
269
203
  assert_equal HTTP::SEE_OTHER, req.response_status
270
204
  assert_equal '/', req.response_headers['Location']
@@ -277,22 +211,14 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
277
211
  end
278
212
 
279
213
  def test_oauth_consent_endpoint_get_no_oauth_signin_id
280
- req = @test_harness.request(
281
- {
282
- ':method' => 'GET',
283
- ':path' => '/oauth/consent',
284
- }
285
- )
214
+ req = get('/oauth/consent')
286
215
  assert_equal HTTP::BAD_REQUEST, req.response_status
287
216
  end
288
217
 
289
218
  def test_oauth_consent_endpoint_get_invalid_oauth_signin_id
290
- req = @test_harness.request(
291
- {
292
- ':method' => 'GET',
293
- ':path' => '/oauth/consent',
294
- 'cookie' => 'outh_signin_id=foo'
295
- }
219
+ req = get(
220
+ '/oauth/consent',
221
+ 'cookie' => 'outh_signin_id=foo'
296
222
  )
297
223
  assert_equal HTTP::BAD_REQUEST, req.response_status
298
224
  end
@@ -321,12 +247,9 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
321
247
  }
322
248
  oauth_signin_id = @store.store(auth_params)
323
249
 
324
- req = @test_harness.request(
325
- {
326
- ':method' => 'GET',
327
- ':path' => '/oauth/consent',
328
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
329
- }
250
+ req = get(
251
+ '/oauth/consent',
252
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
330
253
  )
331
254
  assert_equal HTTP::OK, req.response_status
332
255
  end
@@ -355,16 +278,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
355
278
  }
356
279
  oauth_signin_id = @store.store(auth_params)
357
280
 
358
- req = @test_harness.request(
359
- {
360
- ':method' => 'POST',
361
- ':path' => '/oauth/consent',
362
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
363
- 'content-type' => 'application/x-www-form-urlencoded'
364
- },
365
- URI.encode_www_form(
366
- decision: 'deny',
367
- )
281
+ req = post_form(
282
+ '/oauth/consent',
283
+ { decision: 'deny' },
284
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
368
285
  )
369
286
  assert_equal HTTP::FOUND, req.response_status
370
287
 
@@ -396,16 +313,10 @@ class OAuthPhase3AuthorizationTest < OAuthBaseTest
396
313
  }
397
314
  oauth_signin_id = @store.store(auth_params)
398
315
 
399
- req = @test_harness.request(
400
- {
401
- ':method' => 'POST',
402
- ':path' => '/oauth/consent',
403
- 'cookie' => "oauth_signin_id=#{oauth_signin_id}",
404
- 'content-type' => 'application/x-www-form-urlencoded'
405
- },
406
- URI.encode_www_form(
407
- decision: 'allow',
408
- )
316
+ req = post_form(
317
+ '/oauth/consent',
318
+ { decision: 'allow' },
319
+ 'cookie' => "oauth_signin_id=#{oauth_signin_id}"
409
320
  )
410
321
  assert_equal HTTP::FOUND, req.response_status
411
322
 
@@ -452,19 +363,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
452
363
  end
453
364
 
454
365
  def test_oauth_token_exchange
455
- req = @test_harness.request(
366
+ req = post_form(
367
+ '/oauth/token',
456
368
  {
457
- ':method' => 'POST',
458
- ':path' => '/oauth/token',
459
- 'content-type' => 'application/x-www-form-urlencoded'
460
- },
461
- URI.encode_www_form(
462
- grant_type: 'authorization_code',
463
- code: @auth_code,
464
- redirect_uri: 'http://localhost:4321/callback',
465
- client_id: @client_id,
466
- code_verifier: @code_verifier
467
- )
369
+ grant_type: 'authorization_code',
370
+ code: @auth_code,
371
+ redirect_uri: 'http://localhost:4321/callback',
372
+ client_id: @client_id,
373
+ code_verifier: @code_verifier
374
+ }
468
375
  )
469
376
 
470
377
  assert_equal HTTP::OK, req.response_status
@@ -483,19 +390,8 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
483
390
  end
484
391
 
485
392
  def test_oauth_token_exchange_missing_params
486
- req = @test_harness.request(
487
- {
488
- ':method' => 'POST',
489
- ':path' => '/oauth/token',
490
- 'content-type' => 'application/x-www-form-urlencoded'
491
- },
492
- URI.encode_www_form({}
493
- # grant_type: 'authorization_code',
494
- # code: auth_code,
495
- # redirect_uri: 'http://localhost:4321/callback',
496
- # client_id: client_id,
497
- # code_verifier: code_verifier
498
- )
393
+ req = post_form(
394
+ '/oauth/token', {}
499
395
  )
500
396
 
501
397
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -507,19 +403,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
507
403
  end
508
404
 
509
405
  def test_oauth_token_exchange_invalid_grant_type
510
- req = @test_harness.request(
406
+ req = post_form(
407
+ '/oauth/token',
511
408
  {
512
- ':method' => 'POST',
513
- ':path' => '/oauth/token',
514
- 'content-type' => 'application/x-www-form-urlencoded'
515
- },
516
- URI.encode_www_form(
517
409
  grant_type: 'foo',
518
410
  code: @auth_code,
519
411
  redirect_uri: 'http://localhost:4321/callback',
520
412
  client_id: @client_id,
521
413
  code_verifier: @code_verifier
522
- )
414
+ }
523
415
  )
524
416
 
525
417
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -531,19 +423,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
531
423
  end
532
424
 
533
425
  def test_oauth_token_exchange_invalid_code
534
- req = @test_harness.request(
426
+ req = post_form(
427
+ '/oauth/token',
535
428
  {
536
- ':method' => 'POST',
537
- ':path' => '/oauth/token',
538
- 'content-type' => 'application/x-www-form-urlencoded'
539
- },
540
- URI.encode_www_form(
541
- grant_type: 'authorization_code',
542
- code: @auth_code + '!',
543
- redirect_uri: 'http://localhost:4321/callback',
544
- client_id: @client_id,
545
- code_verifier: @code_verifier
546
- )
429
+ grant_type: 'authorization_code',
430
+ code: @auth_code + '!',
431
+ redirect_uri: 'http://localhost:4321/callback',
432
+ client_id: @client_id,
433
+ code_verifier: @code_verifier
434
+ }
547
435
  )
548
436
 
549
437
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -555,19 +443,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
555
443
  end
556
444
 
557
445
  def test_oauth_token_exchange_invalid_redirect_uri
558
- req = @test_harness.request(
446
+ req = post_form(
447
+ '/oauth/token',
559
448
  {
560
- ':method' => 'POST',
561
- ':path' => '/oauth/token',
562
- 'content-type' => 'application/x-www-form-urlencoded'
563
- },
564
- URI.encode_www_form(
565
- grant_type: 'authorization_code',
566
- code: @auth_code,
567
- redirect_uri: 'http://localhost:4321/foo',
568
- client_id: @client_id,
569
- code_verifier: @code_verifier
570
- )
449
+ grant_type: 'authorization_code',
450
+ code: @auth_code,
451
+ redirect_uri: 'http://localhost:4321/foo',
452
+ client_id: @client_id,
453
+ code_verifier: @code_verifier
454
+ }
571
455
  )
572
456
 
573
457
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -579,19 +463,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
579
463
  end
580
464
 
581
465
  def test_oauth_token_exchange_invalid_client_id
582
- req = @test_harness.request(
466
+ req = post_form(
467
+ '/oauth/token',
583
468
  {
584
- ':method' => 'POST',
585
- ':path' => '/oauth/token',
586
- 'content-type' => 'application/x-www-form-urlencoded'
587
- },
588
- URI.encode_www_form(
589
- grant_type: 'authorization_code',
590
- code: @auth_code + '!',
591
- redirect_uri: 'http://localhost:4321/callback',
592
- client_id: @client_id + 'foo',
593
- code_verifier: @code_verifier
594
- )
469
+ grant_type: 'authorization_code',
470
+ code: @auth_code + '!',
471
+ redirect_uri: 'http://localhost:4321/callback',
472
+ client_id: @client_id + 'foo',
473
+ code_verifier: @code_verifier
474
+ }
595
475
  )
596
476
 
597
477
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -603,19 +483,15 @@ class OAuthPhase4AuthorizationTest < OAuthBaseTest
603
483
  end
604
484
 
605
485
  def test_oauth_token_exchange_invalid_code_verifier
606
- req = @test_harness.request(
486
+ req = post_form(
487
+ '/oauth/token',
607
488
  {
608
- ':method' => 'POST',
609
- ':path' => '/oauth/token',
610
- 'content-type' => 'application/x-www-form-urlencoded'
611
- },
612
- URI.encode_www_form(
613
- grant_type: 'authorization_code',
614
- code: @auth_code + '!',
615
- redirect_uri: 'http://localhost:4321/callback',
616
- client_id: @client_id,
617
- code_verifier: @code_verifier + 'abc'
618
- )
489
+ grant_type: 'authorization_code',
490
+ code: @auth_code + '!',
491
+ redirect_uri: 'http://localhost:4321/callback',
492
+ client_id: @client_id,
493
+ code_verifier: @code_verifier + 'abc'
494
+ }
619
495
  )
620
496
 
621
497
  assert_equal HTTP::BAD_REQUEST, req.response_status
data/lib/syntropy/app.rb CHANGED
@@ -6,7 +6,7 @@ require 'yaml'
6
6
  require 'papercraft'
7
7
 
8
8
  require 'syntropy/errors'
9
- require 'syntropy/module'
9
+ require 'syntropy/module_loader'
10
10
  require 'syntropy/routing_tree'
11
11
  require 'syntropy/mime_types'
12
12
 
@@ -35,7 +35,7 @@ module Syntropy
35
35
  end
36
36
 
37
37
  attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :env
38
- attr_accessor :test_mode
38
+ attr_accessor :raise_on_internal_server_error
39
39
 
40
40
  def initialize(**env)
41
41
  @machine = env[:machine]
@@ -102,6 +102,21 @@ module Syntropy
102
102
  route
103
103
  end
104
104
 
105
+ def setup_db(db_path:, schema_root: '_schema')
106
+ @env[:db_path] = db_path
107
+ @env[:schema_root] = schema_root
108
+
109
+ class << self
110
+ def connection_pool
111
+ @connection_pool ||= DB::ConnectionPool.new(@machine, @env[:db_path], 4)
112
+ end
113
+
114
+ def schema
115
+ @schema ||= DB::Schema.new(module_loader: @module_loader, schema_root: @env[:schema_root])
116
+ end
117
+ end
118
+ end
119
+
105
120
  private
106
121
 
107
122
  # Handles a not found error, taking into account hooks up the tree from the
@@ -458,7 +473,7 @@ module Syntropy
458
473
  req.respond(msg, ':status' => status) rescue nil
459
474
  }
460
475
 
461
- TEST_MODE_DEFAULT_ERROR_HANDLER = ->(req, err) {
476
+ RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER = ->(req, err) {
462
477
  status = Syntropy::Error.http_status(err)
463
478
  raise if status == HTTP::INTERNAL_SERVER_ERROR
464
479
 
@@ -471,8 +486,10 @@ module Syntropy
471
486
  @default_error_handler ||= begin
472
487
  if @builtin_applet
473
488
  @builtin_applet.module_loader.load('/default_error_handler')
489
+ elsif @raise_on_internal_server_error
490
+ RAISE_INTERNAL_SERVER_ERROR_DEFAULT_ERROR_HANDLER
474
491
  else
475
- @test_mode ? TEST_MODE_DEFAULT_ERROR_HANDLER : RAW_DEFAULT_ERROR_HANDLER
492
+ RAW_DEFAULT_ERROR_HANDLER
476
493
  end
477
494
  end
478
495
  end
@@ -482,6 +499,8 @@ module Syntropy
482
499
  #
483
500
  # @return [void]
484
501
  def start
502
+ @module_loader.load('_setup', raise_on_missing: false)
503
+
485
504
  @machine.spin do
486
505
  # we do startup stuff asynchronously, in order to first let Syntropy do
487
506
  # its setup tasks.
@@ -500,9 +519,6 @@ module Syntropy
500
519
  #
501
520
  # @return [void]
502
521
  def file_watcher_loop
503
- wf = @env[:watch_files]
504
- period = wf.is_a?(Numeric) ? wf : 0.1
505
-
506
522
  @machine.file_watch(@root_dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) { |e|
507
523
  fn = e[:fn]
508
524
  @logger&.info(message: 'File change detected', fn: fn)
@@ -510,8 +526,6 @@ module Syntropy
510
526
  debounce_file_change
511
527
  }
512
528
 
513
-
514
-
515
529
  # Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
516
530
  # @logger&.info(message: 'File change detected', fn: fn)
517
531
  # @module_loader.invalidate_fn(fn)
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'extralite'
4
+
5
+ module Syntropy
6
+ module DB
7
+ class ConnectionPool
8
+ attr_reader :count
9
+
10
+ def initialize(machine, fn, max_conn)
11
+ @machine = machine
12
+ @fn = fn
13
+ @count = 0
14
+ @max_conn = max_conn
15
+ @queue = UM::Queue.new
16
+ @key = :"connection_pool_#{object_id}"
17
+ end
18
+
19
+ def with_db
20
+ if (db = Thread.current[@key])
21
+ @machine.snooze
22
+ return yield(db)
23
+ end
24
+
25
+ db = checkout
26
+ begin
27
+ Thread.current[@key] = db
28
+ yield(db)
29
+ ensure
30
+ Thread.current[@key] = nil
31
+ checkin(db)
32
+ end
33
+ end
34
+
35
+ def query(sql, *, **, &)
36
+ with_db { it.query(sql, *, **, &) }
37
+ end
38
+
39
+ def execute(sql, *, **)
40
+ with_db { it.execute(sql, *, **) }
41
+ end
42
+
43
+ def close
44
+ while @queue.count > 0
45
+ db = @machine.shift(@queue)
46
+ db.close
47
+ @count -= 1
48
+ end
49
+ end
50
+
51
+ private
52
+
53
+ def checkout
54
+ return make_db_instance if @queue.count == 0 && @count < @max_conn
55
+
56
+ @machine.shift(@queue)
57
+ end
58
+
59
+ def checkin(db)
60
+ @machine.push(@queue, db)
61
+ end
62
+
63
+ def make_db_instance
64
+ Extralite::Database.new(@fn, wal: true).tap do
65
+ @count += 1
66
+ it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end