mxrb 0.1.2 → 0.1.4

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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/bin/mxrb +458 -10
  3. data/docs/de-DE/README.md +1 -0
  4. data/docs/de-DE/oql-sql.md +42 -0
  5. data/docs/de-DE/project-structure.md +201 -2
  6. data/docs/de-DE/runtime-ruby.md +104 -0
  7. data/docs/de-DE/runtime-schema-migrations.md +44 -0
  8. data/docs/de-DE/validation-matrix.md +42 -4
  9. data/docs/en-US/README.md +1 -0
  10. data/docs/en-US/oql-sql.md +41 -0
  11. data/docs/en-US/project-structure.md +192 -2
  12. data/docs/en-US/runtime-ruby.md +100 -0
  13. data/docs/en-US/runtime-schema-migrations.md +41 -0
  14. data/docs/en-US/validation-matrix.md +39 -4
  15. data/docs/pt-BR/README.md +1 -0
  16. data/docs/pt-BR/entity-dsl.md +8 -0
  17. data/docs/pt-BR/oql-sql.md +41 -0
  18. data/docs/pt-BR/project-structure.md +197 -2
  19. data/docs/pt-BR/runtime-ruby.md +108 -0
  20. data/docs/pt-BR/runtime-schema-migrations.md +40 -0
  21. data/docs/pt-BR/validation-matrix.md +42 -5
  22. data/docs/pt-BR/writing.md +28 -0
  23. data/lib/mxrb/cli/help.rb +426 -0
  24. data/lib/mxrb/cli/release.rb +137 -0
  25. data/lib/mxrb/compiler/compatibility_analyzer.rb +1 -1
  26. data/lib/mxrb/compiler/data_grid_bundle_compiler.rb +8 -1
  27. data/lib/mxrb/compiler/domain_document_compiler.rb +3 -0
  28. data/lib/mxrb/compiler/gallery_bundle_compiler.rb +11 -1
  29. data/lib/mxrb/compiler/generic_widget_bundle_compiler.rb +76 -19
  30. data/lib/mxrb/compiler/legacy_custom_widget_compiler.rb +159 -0
  31. data/lib/mxrb/compiler/legacy_data_grid_compiler.rb +43 -7
  32. data/lib/mxrb/compiler/legacy_page_builder.rb +1248 -23
  33. data/lib/mxrb/compiler/packager.rb +27 -13
  34. data/lib/mxrb/compiler/page_bundle_compiler.rb +13 -4
  35. data/lib/mxrb/compiler/portable_packager.rb +16 -11
  36. data/lib/mxrb/compiler/web_bundle_builder.rb +5 -1
  37. data/lib/mxrb/compiler/web_shell_materializer.rb +42 -2
  38. data/lib/mxrb/compiler/widget_package_extractor.rb +3 -0
  39. data/lib/mxrb/domain_diagram/lifecycle.rb +272 -0
  40. data/lib/mxrb/domain_diagram.rb +409 -0
  41. data/lib/mxrb/dsl/builder.rb +62 -11
  42. data/lib/mxrb/environment.rb +226 -0
  43. data/lib/mxrb/exporter.rb +265 -45
  44. data/lib/mxrb/http/server.rb +107 -0
  45. data/lib/mxrb/initializer.rb +80 -9
  46. data/lib/mxrb/integrity/validator.rb +32 -7
  47. data/lib/mxrb/io/bson_codec.rb +42 -1
  48. data/lib/mxrb/io/mpr_file.rb +146 -3
  49. data/lib/mxrb/model/attribute.rb +3 -0
  50. data/lib/mxrb/model/entity.rb +44 -2
  51. data/lib/mxrb/model/microflow.rb +3 -1
  52. data/lib/mxrb/model/module.rb +71 -0
  53. data/lib/mxrb/model/page.rb +126 -15
  54. data/lib/mxrb/model/project.rb +3 -0
  55. data/lib/mxrb/modeler/catalog.rb +178 -0
  56. data/lib/mxrb/official_marketplace/module_package_importer.rb +23 -12
  57. data/lib/mxrb/official_marketplace/widget_package_installer.rb +15 -11
  58. data/lib/mxrb/official_marketplace.rb +9 -3
  59. data/lib/mxrb/oql/reverse_translator.rb +394 -0
  60. data/lib/mxrb/oql/server.rb +4 -8
  61. data/lib/mxrb/oql.rb +32 -4
  62. data/lib/mxrb/progress.rb +242 -0
  63. data/lib/mxrb/project_lifecycle.rb +5 -2
  64. data/lib/mxrb/ruby_app/exporter.rb +1749 -0
  65. data/lib/mxrb/ruby_app/preset.rb +506 -0
  66. data/lib/mxrb/ruby_app/session_manager.rb +122 -0
  67. data/lib/mxrb/ruby_app.rb +1361 -0
  68. data/lib/mxrb/runtime/access_control.rb +408 -0
  69. data/lib/mxrb/runtime/native.rb +1015 -93
  70. data/lib/mxrb/runtime/scheduler.rb +396 -0
  71. data/lib/mxrb/runtime/schema_migrator.rb +598 -0
  72. data/lib/mxrb/runtime/shared_store.rb +335 -0
  73. data/lib/mxrb/runtime/sqlite_store.rb +729 -0
  74. data/lib/mxrb/scaffold/generator.rb +11 -3
  75. data/lib/mxrb/team_server.rb +10 -7
  76. data/lib/mxrb/uml/activity_diagram.rb +118 -0
  77. data/lib/mxrb/uml/class_diagram.rb +118 -0
  78. data/lib/mxrb/uml/sequence_diagram.rb +112 -0
  79. data/lib/mxrb/uml/server.rb +146 -0
  80. data/lib/mxrb/uml/support.rb +35 -0
  81. data/lib/mxrb/version.rb +1 -1
  82. data/lib/mxrb/web_ui/assets/abnfDiagram-N423BO3Z-C4JD3cEC.js +1 -0
  83. data/lib/mxrb/web_ui/assets/arc-3Z53kgFp.js +1 -0
  84. data/lib/mxrb/web_ui/assets/architecture-TIHT7OUA-BStBTLea.js +1 -0
  85. data/lib/mxrb/web_ui/assets/architectureDiagram-T3A2C74G-DBwqnM4J.js +36 -0
  86. data/lib/mxrb/web_ui/assets/array-BifhSqXX.js +1 -0
  87. data/lib/mxrb/web_ui/assets/blockDiagram-VBNYF7ZC-DLtuHt-j.js +132 -0
  88. data/lib/mxrb/web_ui/assets/c4Diagram-5PPSVZJV-ConSk_pe.js +10 -0
  89. data/lib/mxrb/web_ui/assets/channel-BhPvD7Dt.js +1 -0
  90. data/lib/mxrb/web_ui/assets/chunk-2GRJ4B5K-D2HpWsf_.js +1 -0
  91. data/lib/mxrb/web_ui/assets/chunk-2Q5K7J3B-C1jixKkw.js +1 -0
  92. data/lib/mxrb/web_ui/assets/chunk-4I5QYGJK-CiIdNwaF.js +1 -0
  93. data/lib/mxrb/web_ui/assets/chunk-5RXB4S5H-Bu_AaEkE.js +231 -0
  94. data/lib/mxrb/web_ui/assets/chunk-5VM5RSS4-ZNzvKenW.js +15 -0
  95. data/lib/mxrb/web_ui/assets/chunk-6Q2QTUOP-wQkNmt_X.js +88 -0
  96. data/lib/mxrb/web_ui/assets/chunk-7BUUIJ7U-Bb538aSH.js +1 -0
  97. data/lib/mxrb/web_ui/assets/chunk-GF5L2VYU-G8MmiBzU.js +206 -0
  98. data/lib/mxrb/web_ui/assets/chunk-I66GZJ75-CulcrLPh.js +127 -0
  99. data/lib/mxrb/web_ui/assets/chunk-JQJVKLGR-ukjzcdlL.js +156 -0
  100. data/lib/mxrb/web_ui/assets/chunk-JWPE2WC7-DVXcaiue.js +1 -0
  101. data/lib/mxrb/web_ui/assets/chunk-KBJHAD2P-Wm35lYov.js +1 -0
  102. data/lib/mxrb/web_ui/assets/chunk-KEIR6QF5-BICK3FdT.js +161 -0
  103. data/lib/mxrb/web_ui/assets/chunk-NSK5VX7P-D2nYrCY4.js +2 -0
  104. data/lib/mxrb/web_ui/assets/chunk-QR6OTTB3-BrdIzg4Q.js +62 -0
  105. data/lib/mxrb/web_ui/assets/chunk-RYQCIY6F-CCfZMcW6.js +1 -0
  106. data/lib/mxrb/web_ui/assets/chunk-UBXNYLIW-BPEVYsFK.js +1 -0
  107. data/lib/mxrb/web_ui/assets/chunk-W5SLKNZC-2E6OPhA4.js +1 -0
  108. data/lib/mxrb/web_ui/assets/chunk-WRU74C26-CBIvOyWp.js +70 -0
  109. data/lib/mxrb/web_ui/assets/chunk-XXDRQBXY-pH58XAyl.js +1 -0
  110. data/lib/mxrb/web_ui/assets/chunk-Y2CYZVJY-DsF7k-Jl.js +1 -0
  111. data/lib/mxrb/web_ui/assets/classDiagram-JCYQIIEL-xiqo4D89.js +1 -0
  112. data/lib/mxrb/web_ui/assets/classDiagram-v2-OCEON4UE-xiqo4D89.js +1 -0
  113. data/lib/mxrb/web_ui/assets/cose-bilkent-JH36ORCC-t_flPpii.js +1 -0
  114. data/lib/mxrb/web_ui/assets/cynefin-VYW2F7L2-BIqktTEv.js +1 -0
  115. data/lib/mxrb/web_ui/assets/cynefinDiagram-MW4NZA55-C2d--lut.js +62 -0
  116. data/lib/mxrb/web_ui/assets/cytoscape.esm-B-NFISlW.js +321 -0
  117. data/lib/mxrb/web_ui/assets/dagre-Buvkdvvj.js +1 -0
  118. data/lib/mxrb/web_ui/assets/dagre-VZM6K2ZE-CSz2_tb5.js +4 -0
  119. data/lib/mxrb/web_ui/assets/defaultLocale-BFoDCU3G.js +1 -0
  120. data/lib/mxrb/web_ui/assets/diagram-7IWD3JNH-DCt7u7V_.js +30 -0
  121. data/lib/mxrb/web_ui/assets/diagram-B4RE2ZJO-DqZBqkMu.js +3 -0
  122. data/lib/mxrb/web_ui/assets/diagram-LBJQPF4R-ClIYbIsI.js +24 -0
  123. data/lib/mxrb/web_ui/assets/diagram-Q27KOJAE-DFxhvthJ.js +24 -0
  124. data/lib/mxrb/web_ui/assets/diagram-UB23O5K3-BSUTXptv.js +41 -0
  125. data/lib/mxrb/web_ui/assets/dist-D2qOEbeJ.js +1 -0
  126. data/lib/mxrb/web_ui/assets/domain-mGOZ1KCG.css +1 -0
  127. data/lib/mxrb/web_ui/assets/domain-qsjXhy4v.js +1 -0
  128. data/lib/mxrb/web_ui/assets/ebnfDiagram-BXEA7PRR-B5reICEi.js +1 -0
  129. data/lib/mxrb/web_ui/assets/erDiagram-JOGREHBK-Co5Mxn2p.js +85 -0
  130. data/lib/mxrb/web_ui/assets/eventmodeling-45OFAUF4-C1ylhSks.js +1 -0
  131. data/lib/mxrb/web_ui/assets/flowDiagram-UKHOOZJN-CHXaJIaS.js +1 -0
  132. data/lib/mxrb/web_ui/assets/ganttDiagram-PKOTCBZU-DPPPaWaz.js +292 -0
  133. data/lib/mxrb/web_ui/assets/gitGraph-TEB2WS4Q-qS77ywA9.js +1 -0
  134. data/lib/mxrb/web_ui/assets/gitGraphDiagram-DS77QQ5N-DpAv6-BT.js +106 -0
  135. data/lib/mxrb/web_ui/assets/graphlib-DS17s2tU.js +1 -0
  136. data/lib/mxrb/web_ui/assets/info-DKCQHKI2-C9VD1Y4e.js +1 -0
  137. data/lib/mxrb/web_ui/assets/infoDiagram-6WML65LV-Bk1kbcCy.js +2 -0
  138. data/lib/mxrb/web_ui/assets/init-C-OQMol4.js +1 -0
  139. data/lib/mxrb/web_ui/assets/ishikawaDiagram-WSZJBQD7-0gcISBFR.js +70 -0
  140. data/lib/mxrb/web_ui/assets/journeyDiagram-NVQOT4AX-Dt0RQnlh.js +139 -0
  141. data/lib/mxrb/web_ui/assets/jsx-runtime-DV0a5kSb.js +9 -0
  142. data/lib/mxrb/web_ui/assets/kanban-definition-27J2QSJJ-DIPQM7w9.js +89 -0
  143. data/lib/mxrb/web_ui/assets/katex-CXMH3UgJ.js +257 -0
  144. data/lib/mxrb/web_ui/assets/line-C0rYD-tL.js +1 -0
  145. data/lib/mxrb/web_ui/assets/linear-BPPxWorj.js +1 -0
  146. data/lib/mxrb/web_ui/assets/map-BaFkSB1l.js +1 -0
  147. data/lib/mxrb/web_ui/assets/mermaid-parser.core-B_UPzTxa.js +7 -0
  148. data/lib/mxrb/web_ui/assets/mindmap-definition-FAOFIHXS-CdJRSMuk.js +96 -0
  149. data/lib/mxrb/web_ui/assets/modeler-BNqHthwk.css +1 -0
  150. data/lib/mxrb/web_ui/assets/modeler-CQmqflbg.js +1 -0
  151. data/lib/mxrb/web_ui/assets/ordinal-BDEzSJ7C.js +1 -0
  152. data/lib/mxrb/web_ui/assets/packet-7NZHBO7P-CwQ4NSBs.js +1 -0
  153. data/lib/mxrb/web_ui/assets/path-fybaL0A-.js +1 -0
  154. data/lib/mxrb/web_ui/assets/pegDiagram-VL7TDLO6-D40S7ppx.js +1 -0
  155. data/lib/mxrb/web_ui/assets/pie-RZYD4A2V-CJroHMKh.js +1 -0
  156. data/lib/mxrb/web_ui/assets/pieDiagram-7S7Q4E2Y-QB54__BV.js +39 -0
  157. data/lib/mxrb/web_ui/assets/quadrantDiagram-CIZ2JOQS-DBhDS6ag.js +7 -0
  158. data/lib/mxrb/web_ui/assets/radar-I7S5WNFK-DdIUwd2d.js +1 -0
  159. data/lib/mxrb/web_ui/assets/railroad-3IZDKUUU-B1Lw6Xyq.js +1 -0
  160. data/lib/mxrb/web_ui/assets/railroad-abnf-AHOZXSZD-CQgQoeak.js +1 -0
  161. data/lib/mxrb/web_ui/assets/railroad-ebnf-EBAXGLYW-D7qEQ2x8.js +1 -0
  162. data/lib/mxrb/web_ui/assets/railroad-peg-LSFZ7HO6-C005Vxbb.js +1 -0
  163. data/lib/mxrb/web_ui/assets/railroadDiagram-AXF67PYL-BBP2qk7M.js +1 -0
  164. data/lib/mxrb/web_ui/assets/requirementDiagram-LRYGKXZP-D2cLsAoI.js +84 -0
  165. data/lib/mxrb/web_ui/assets/rough.esm-Dy-Kn_BL.js +1 -0
  166. data/lib/mxrb/web_ui/assets/sankeyDiagram-W5VNT64P-Bpi3FiP2.js +40 -0
  167. data/lib/mxrb/web_ui/assets/sequenceDiagram-SI44F4Z6-DCRcDlVe.js +162 -0
  168. data/lib/mxrb/web_ui/assets/sizeCapture-X5ZJPWSS-B0uUizjq.js +1 -0
  169. data/lib/mxrb/web_ui/assets/src-CvxfUak2.js +1 -0
  170. data/lib/mxrb/web_ui/assets/stateDiagram-OKZ733FA-C2GCEoxl.js +1 -0
  171. data/lib/mxrb/web_ui/assets/stateDiagram-v2-UEYNNEHI-D2gWgedh.js +1 -0
  172. data/lib/mxrb/web_ui/assets/swimlanes-SLNWSIFB-CRI9irEW.js +2 -0
  173. data/lib/mxrb/web_ui/assets/swimlanesDiagram-ULZ7WXOC-C5Wb_-hZ.js +8 -0
  174. data/lib/mxrb/web_ui/assets/theme-_mxc7BdY.css +1 -0
  175. data/lib/mxrb/web_ui/assets/timeline-definition-Z64GVDOM-B1-mNg-h.js +120 -0
  176. data/lib/mxrb/web_ui/assets/treeView-QDETBFTQ-7CTvIwKq.js +1 -0
  177. data/lib/mxrb/web_ui/assets/treemap-6X3UGDF4-CtL22pq5.js +1 -0
  178. data/lib/mxrb/web_ui/assets/uml-C3KAFGTq.js +51 -0
  179. data/lib/mxrb/web_ui/assets/uml-Dn-qd01X.css +1 -0
  180. data/lib/mxrb/web_ui/assets/vennDiagram-T6HMQDX7-BzVnUEAy.js +34 -0
  181. data/lib/mxrb/web_ui/assets/wardley-OPB4EBWU-JNi-QNag.js +1 -0
  182. data/lib/mxrb/web_ui/assets/wardleyDiagram-T6FBY63Y-BUwmAQ-B.js +78 -0
  183. data/lib/mxrb/web_ui/assets/xychartDiagram-ELKLHX3M-DEQS8sKp.js +7 -0
  184. data/lib/mxrb/web_ui/domain.html +15 -0
  185. data/lib/mxrb/web_ui/modeler.html +16 -0
  186. data/lib/mxrb/web_ui/uml.html +31 -0
  187. data/lib/mxrb/web_ui.rb +89 -0
  188. data/lib/mxrb/widget_certification.rb +208 -0
  189. data/lib/mxrb/writer.rb +278 -41
  190. data/lib/mxrb.rb +38 -8
  191. metadata +157 -5
@@ -0,0 +1,335 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fileutils'
4
+ require 'digest'
5
+ require 'json'
6
+ require 'sqlite3'
7
+
8
+ module Mxrb
9
+ module Runtime
10
+ # rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
11
+ # rubocop:disable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists
12
+ SessionRecord = Data.define(:token, :identity, :expires_at)
13
+
14
+ # Injectable session persistence protocol.
15
+ module SessionStore
16
+ def write_session(token:, identity:, expires_at:) = raise NotImplementedError
17
+ def read_session(token, now: Time.now.utc) = raise NotImplementedError
18
+ def delete_session(token) = raise NotImplementedError
19
+ end
20
+
21
+ # Injectable scheduled-event claim/lease protocol. Renewal is optional for
22
+ # local coordinators and required for distributed lease implementations.
23
+ module SchedulerCoordinator
24
+ def claim_scheduled_event(event:, slot:, owner:, now:, lease_until:, skip_overlap:)
25
+ raise NotImplementedError
26
+ end
27
+
28
+ def complete_scheduled_event(event:, slot:, owner:, now: Time.now.utc) = raise NotImplementedError
29
+ def renew_scheduled_event(event:, slot:, owner:, lease_until:) = raise NotImplementedError
30
+ end
31
+
32
+ # Process-local implementation of the shared runtime contracts. It keeps
33
+ # the standalone runtime dependency-free while exposing the same API as
34
+ # SQLiteSharedStore for injection in SessionManager and Scheduler.
35
+ class MemorySharedStore
36
+ include SessionStore
37
+ include SchedulerCoordinator
38
+
39
+ def initialize
40
+ @sessions = {}
41
+ @claims = {}
42
+ @leases = {}
43
+ @mutex = Mutex.new
44
+ end
45
+
46
+ def write_session(token:, identity:, expires_at:)
47
+ @mutex.synchronize do
48
+ @sessions[token.to_s] = SessionRecord.new(
49
+ token.to_s, copy(identity), expires_at.utc
50
+ )
51
+ end
52
+ token
53
+ end
54
+
55
+ def read_session(token, now: Time.now.utc)
56
+ @mutex.synchronize do
57
+ prune_sessions(now)
58
+ record = @sessions[token.to_s]
59
+ record && SessionRecord.new(record.token, copy(record.identity), record.expires_at)
60
+ end
61
+ end
62
+
63
+ def delete_session(token)
64
+ @mutex.synchronize { !!@sessions.delete(token.to_s) }
65
+ end
66
+
67
+ def claim_scheduled_event(event:, slot:, owner:, now:, lease_until:, skip_overlap:)
68
+ key = [event.to_s, slot.to_s]
69
+ @mutex.synchronize do
70
+ claim = @claims[key]
71
+ return false if claim && (claim[:completed_at] || claim.fetch(:lease_expires_at) > now)
72
+
73
+ lease = @leases[event.to_s]
74
+ return false if skip_overlap && lease && lease.fetch(:expires_at) > now
75
+
76
+ @claims[key] = {
77
+ owner: owner.to_s, claimed_at: now, lease_expires_at: lease_until,
78
+ completed_at: nil
79
+ }
80
+ if skip_overlap
81
+ @leases[event.to_s] = {
82
+ slot: slot.to_s, owner: owner.to_s, expires_at: lease_until
83
+ }
84
+ end
85
+ true
86
+ end
87
+ end
88
+
89
+ def complete_scheduled_event(event:, slot:, owner:, now: Time.now.utc)
90
+ key = [event.to_s, slot.to_s]
91
+ @mutex.synchronize do
92
+ claim = @claims[key]
93
+ claim[:completed_at] = now if claim && claim.fetch(:owner) == owner.to_s
94
+ lease = @leases[event.to_s]
95
+ @leases.delete(event.to_s) \
96
+ if lease && lease.fetch(:owner) == owner.to_s && lease.fetch(:slot) == slot.to_s
97
+ end
98
+ true
99
+ end
100
+
101
+ def renew_scheduled_event(event:, slot:, owner:, lease_until:)
102
+ key = [event.to_s, slot.to_s]
103
+ @mutex.synchronize do
104
+ claim = @claims[key]
105
+ return false unless claim && claim.fetch(:owner) == owner.to_s && !claim[:completed_at]
106
+
107
+ claim[:lease_expires_at] = lease_until
108
+ lease = @leases[event.to_s]
109
+ lease[:expires_at] = lease_until \
110
+ if lease && lease.fetch(:owner) == owner.to_s && lease.fetch(:slot) == slot.to_s
111
+ true
112
+ end
113
+ end
114
+
115
+ def close = nil
116
+
117
+ private
118
+
119
+ def prune_sessions(now)
120
+ @sessions.delete_if { |_token, record| record.expires_at <= now }
121
+ end
122
+
123
+ def copy(value) = JSON.parse(JSON.generate(value))
124
+ end
125
+
126
+ # SQLite implementation safe for multiple Ruby processes sharing a file.
127
+ # BEGIN IMMEDIATE serializes claim decisions; primary keys make a schedule
128
+ # slot idempotent even after a process restart.
129
+ class SQLiteSharedStore
130
+ include SessionStore
131
+ include SchedulerCoordinator
132
+
133
+ attr_reader :database
134
+
135
+ def initialize(path, busy_timeout: 5_000)
136
+ raw_path = path.to_s
137
+ raise ArgumentError, 'shared store path must not be empty' if raw_path.empty?
138
+
139
+ expanded = raw_path == ':memory:' ? raw_path : File.expand_path(raw_path)
140
+
141
+ FileUtils.mkdir_p(File.dirname(expanded)) unless expanded == ':memory:'
142
+ @database = SQLite3::Database.new(expanded)
143
+ @database.results_as_hash = true
144
+ @database.busy_timeout = Integer(busy_timeout)
145
+ @mutex = Mutex.new
146
+ configure!
147
+ migrate!
148
+ end
149
+
150
+ def write_session(token:, identity:, expires_at:)
151
+ synchronize do
152
+ database.execute(
153
+ <<~SQL, [session_key(token), JSON.generate(identity), timestamp(expires_at)]
154
+ INSERT INTO mxrb_runtime_sessions (token, identity_json, expires_at)
155
+ VALUES (?, ?, ?)
156
+ ON CONFLICT(token) DO UPDATE SET
157
+ identity_json = excluded.identity_json,
158
+ expires_at = excluded.expires_at
159
+ SQL
160
+ )
161
+ end
162
+ token
163
+ end
164
+
165
+ def read_session(token, now: Time.now.utc)
166
+ row = synchronize do
167
+ database.execute('DELETE FROM mxrb_runtime_sessions WHERE expires_at <= ?', [timestamp(now)])
168
+ database.get_first_row(
169
+ 'SELECT token, identity_json, expires_at FROM mxrb_runtime_sessions WHERE token = ?',
170
+ [session_key(token)]
171
+ )
172
+ end
173
+ return unless row
174
+
175
+ SessionRecord.new(
176
+ token.to_s, JSON.parse(row.fetch('identity_json')),
177
+ Time.at(row.fetch('expires_at').to_f).utc
178
+ )
179
+ end
180
+
181
+ def delete_session(token)
182
+ synchronize do
183
+ database.execute('DELETE FROM mxrb_runtime_sessions WHERE token = ?', [session_key(token)])
184
+ database.changes.positive?
185
+ end
186
+ end
187
+
188
+ def claim_scheduled_event(event:, slot:, owner:, now:, lease_until:, skip_overlap:)
189
+ transaction do
190
+ claim = database.get_first_row(
191
+ <<~SQL, [event.to_s, slot.to_s]
192
+ SELECT completed_at, lease_expires_at
193
+ FROM mxrb_runtime_scheduled_claims
194
+ WHERE event_key = ? AND slot = ?
195
+ SQL
196
+ )
197
+ next false if claim && (claim['completed_at'] || claim.fetch('lease_expires_at').to_f > timestamp(now))
198
+
199
+ if skip_overlap
200
+ active = database.get_first_value(
201
+ 'SELECT 1 FROM mxrb_runtime_scheduler_leases WHERE event_key = ? AND expires_at > ?',
202
+ [event.to_s, timestamp(now)]
203
+ )
204
+ next false if active
205
+ end
206
+
207
+ database.execute(
208
+ <<~SQL, [event.to_s, slot.to_s, owner.to_s, timestamp(now), timestamp(lease_until)]
209
+ INSERT INTO mxrb_runtime_scheduled_claims
210
+ (event_key, slot, owner, claimed_at, lease_expires_at, completed_at)
211
+ VALUES (?, ?, ?, ?, ?, NULL)
212
+ ON CONFLICT(event_key, slot) DO UPDATE SET
213
+ owner = excluded.owner,
214
+ claimed_at = excluded.claimed_at,
215
+ lease_expires_at = excluded.lease_expires_at,
216
+ completed_at = NULL
217
+ SQL
218
+ )
219
+ if skip_overlap
220
+ database.execute(
221
+ <<~SQL, [event.to_s, slot.to_s, owner.to_s, timestamp(lease_until)]
222
+ INSERT INTO mxrb_runtime_scheduler_leases (event_key, slot, owner, expires_at)
223
+ VALUES (?, ?, ?, ?)
224
+ ON CONFLICT(event_key) DO UPDATE SET
225
+ slot = excluded.slot,
226
+ owner = excluded.owner,
227
+ expires_at = excluded.expires_at
228
+ SQL
229
+ )
230
+ end
231
+ true
232
+ end
233
+ end
234
+
235
+ def complete_scheduled_event(event:, slot:, owner:, now: Time.now.utc)
236
+ transaction do
237
+ database.execute(
238
+ <<~SQL, [timestamp(now), event.to_s, slot.to_s, owner.to_s]
239
+ UPDATE mxrb_runtime_scheduled_claims
240
+ SET completed_at = ?
241
+ WHERE event_key = ? AND slot = ? AND owner = ?
242
+ SQL
243
+ )
244
+ database.execute(
245
+ <<~SQL, [event.to_s, slot.to_s, owner.to_s]
246
+ DELETE FROM mxrb_runtime_scheduler_leases
247
+ WHERE event_key = ? AND slot = ? AND owner = ?
248
+ SQL
249
+ )
250
+ end
251
+ true
252
+ end
253
+
254
+ def renew_scheduled_event(event:, slot:, owner:, lease_until:)
255
+ transaction do
256
+ database.execute(
257
+ <<~SQL, [timestamp(lease_until), event.to_s, slot.to_s, owner.to_s]
258
+ UPDATE mxrb_runtime_scheduled_claims
259
+ SET lease_expires_at = ?
260
+ WHERE event_key = ? AND slot = ? AND owner = ? AND completed_at IS NULL
261
+ SQL
262
+ )
263
+ next false unless database.changes.positive?
264
+
265
+ database.execute(
266
+ <<~SQL, [timestamp(lease_until), event.to_s, slot.to_s, owner.to_s]
267
+ UPDATE mxrb_runtime_scheduler_leases
268
+ SET expires_at = ?
269
+ WHERE event_key = ? AND slot = ? AND owner = ?
270
+ SQL
271
+ )
272
+ true
273
+ end
274
+ end
275
+
276
+ def close
277
+ @mutex.synchronize { database.close unless database.closed? }
278
+ end
279
+
280
+ private
281
+
282
+ def configure!
283
+ database.execute('PRAGMA journal_mode = WAL')
284
+ database.execute('PRAGMA synchronous = NORMAL')
285
+ end
286
+
287
+ def migrate!
288
+ synchronize do
289
+ database.execute_batch(<<~SQL)
290
+ CREATE TABLE IF NOT EXISTS mxrb_runtime_sessions (
291
+ token TEXT PRIMARY KEY,
292
+ identity_json TEXT NOT NULL,
293
+ expires_at REAL NOT NULL
294
+ );
295
+ CREATE INDEX IF NOT EXISTS mxrb_runtime_sessions_expiration
296
+ ON mxrb_runtime_sessions (expires_at);
297
+ CREATE TABLE IF NOT EXISTS mxrb_runtime_scheduled_claims (
298
+ event_key TEXT NOT NULL,
299
+ slot TEXT NOT NULL,
300
+ owner TEXT NOT NULL,
301
+ claimed_at REAL NOT NULL,
302
+ lease_expires_at REAL NOT NULL,
303
+ completed_at REAL,
304
+ PRIMARY KEY (event_key, slot)
305
+ );
306
+ CREATE TABLE IF NOT EXISTS mxrb_runtime_scheduler_leases (
307
+ event_key TEXT PRIMARY KEY,
308
+ slot TEXT NOT NULL,
309
+ owner TEXT NOT NULL,
310
+ expires_at REAL NOT NULL
311
+ );
312
+ SQL
313
+ end
314
+ end
315
+
316
+ def transaction
317
+ synchronize do
318
+ database.execute('BEGIN IMMEDIATE')
319
+ result = yield
320
+ database.execute('COMMIT')
321
+ result
322
+ rescue Exception # rubocop:disable Lint/RescueException
323
+ database.execute('ROLLBACK') if database.transaction_active?
324
+ raise
325
+ end
326
+ end
327
+
328
+ def synchronize(&block) = @mutex.synchronize(&block)
329
+ def timestamp(value) = value.to_f
330
+ def session_key(token) = Digest::SHA256.hexdigest(token.to_s)
331
+ end
332
+ # rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength
333
+ # rubocop:enable Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists
334
+ end
335
+ end