solid_objects 0.4.2 → 0.5.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +62 -10
- data/app/assets/javascripts/solid_objects/component_refresh.js +124 -0
- data/app/controllers/solid_objects/components_controller.rb +2 -7
- data/app/helpers/solid_objects/actor_helper.rb +8 -1
- data/docs/adr/0009-realtime-updates.md +18 -1
- data/docs/architecture.md +34 -18
- data/docs/authorization.md +24 -0
- data/docs/correctness.md +19 -8
- data/docs/operations.md +6 -0
- data/docs/realtime.md +78 -13
- data/docs/roadmap.md +6 -5
- data/examples/application/app/views/chat_rooms/show.html.erb +3 -1
- data/lib/solid_objects/activation.rb +9 -5
- data/lib/solid_objects/actor_registry.rb +6 -1
- data/lib/solid_objects/actor_view.rb +40 -13
- data/lib/solid_objects/application_actor_loader.rb +53 -0
- data/lib/solid_objects/caller_process.rb +6 -4
- data/lib/solid_objects/cli.rb +2 -0
- data/lib/solid_objects/client.rb +26 -20
- data/lib/solid_objects/component_registration.rb +61 -16
- data/lib/solid_objects/component_renderer.rb +14 -17
- data/lib/solid_objects/component_subscriptions.rb +7 -7
- data/lib/solid_objects/component_token.rb +70 -2
- data/lib/solid_objects/database_adapter.rb +10 -0
- data/lib/solid_objects/database_adapters/sqlite.rb +51 -2
- data/lib/solid_objects/dom_identity.rb +12 -3
- data/lib/solid_objects/engine.rb +7 -0
- data/lib/solid_objects/process_registry.rb +21 -14
- data/lib/solid_objects/sync_diagnostics.rb +57 -3
- data/lib/solid_objects/synchronous_invocation.rb +45 -13
- data/lib/solid_objects/turbo_stream_renderer.rb +14 -5
- data/lib/solid_objects/version.rb +1 -1
- data/sig/generated/lib/solid_objects/actor_registry.rbs +3 -0
- data/sig/generated/lib/solid_objects/actor_view.rbs +10 -4
- data/sig/generated/lib/solid_objects/application_actor_loader.rbs +30 -0
- data/sig/generated/lib/solid_objects/component_registration.rbs +34 -10
- data/sig/generated/lib/solid_objects/component_renderer.rbs +4 -8
- data/sig/generated/lib/solid_objects/component_token.rbs +24 -2
- data/sig/generated/lib/solid_objects/database_adapter.rbs +8 -2
- data/sig/generated/lib/solid_objects/database_adapters/sqlite.rbs +13 -0
- data/sig/generated/lib/solid_objects/dom_identity.rbs +5 -2
- data/sig/generated/lib/solid_objects/sync_diagnostics.rbs +9 -0
- data/sig/generated/lib/solid_objects/synchronous_invocation.rbs +9 -0
- data/sig/generated/lib/solid_objects/turbo_stream_renderer.rbs +3 -0
- metadata +4 -1
|
@@ -3,46 +3,44 @@
|
|
|
3
3
|
module SolidObjects
|
|
4
4
|
class ComponentRenderer
|
|
5
5
|
# @rbs @snapshot: ActorSnapshot
|
|
6
|
-
# @rbs @
|
|
7
|
-
# @rbs @dependencies: Array[String]
|
|
6
|
+
# @rbs @registration: ComponentRegistration
|
|
8
7
|
# @rbs @view_context: untyped
|
|
9
8
|
# @rbs @authorization_context: untyped
|
|
10
9
|
|
|
11
|
-
# @rbs (snapshot: ActorSnapshot,
|
|
10
|
+
# @rbs (snapshot: ActorSnapshot, registration: ComponentRegistration, view_context: untyped, authorization_context: untyped) -> void
|
|
12
11
|
def initialize(
|
|
13
12
|
snapshot:,
|
|
14
|
-
|
|
15
|
-
dependencies:,
|
|
13
|
+
registration:,
|
|
16
14
|
view_context:,
|
|
17
15
|
authorization_context:
|
|
18
16
|
)
|
|
19
17
|
@snapshot = snapshot
|
|
20
|
-
@
|
|
21
|
-
@dependencies = dependencies
|
|
18
|
+
@registration = registration
|
|
22
19
|
@view_context = view_context
|
|
23
20
|
@authorization_context = authorization_context
|
|
24
21
|
end
|
|
25
22
|
|
|
26
23
|
# @rbs () -> untyped
|
|
27
24
|
def call
|
|
28
|
-
[ component_name, *dependencies ].uniq.each do |authorization_name|
|
|
25
|
+
[ registration.component_name, *registration.dependencies ].uniq.each do |authorization_name|
|
|
29
26
|
authorize_read!(authorization_name)
|
|
30
27
|
end
|
|
31
28
|
actor = ComponentView.new(
|
|
32
29
|
snapshot:,
|
|
33
|
-
dependencies
|
|
30
|
+
dependencies: registration.dependencies,
|
|
34
31
|
authorization_context:
|
|
35
32
|
)
|
|
36
33
|
view_context.render(
|
|
37
34
|
partial: default_partial,
|
|
38
|
-
locals:
|
|
35
|
+
locals: registration.locals.transform_keys(&:to_sym).merge(
|
|
39
36
|
actor:,
|
|
40
|
-
authorization_context
|
|
41
|
-
|
|
37
|
+
authorization_context:,
|
|
38
|
+
component_key: registration.component_key
|
|
39
|
+
)
|
|
42
40
|
)
|
|
43
41
|
rescue ActionView::MissingTemplate
|
|
44
42
|
raise UnknownComponent,
|
|
45
|
-
"unknown component #{component_name.inspect} for #{snapshot.reference.actor_type}"
|
|
43
|
+
"unknown component #{registration.component_name.inspect} for #{snapshot.reference.actor_type}"
|
|
46
44
|
rescue ActionView::Template::Error => error
|
|
47
45
|
raise error.cause if error.cause.is_a?(SolidObjects::Error)
|
|
48
46
|
|
|
@@ -52,8 +50,7 @@ module SolidObjects
|
|
|
52
50
|
private
|
|
53
51
|
|
|
54
52
|
attr_reader :snapshot,
|
|
55
|
-
:
|
|
56
|
-
:dependencies,
|
|
53
|
+
:registration,
|
|
57
54
|
:view_context,
|
|
58
55
|
:authorization_context
|
|
59
56
|
|
|
@@ -63,7 +60,7 @@ module SolidObjects
|
|
|
63
60
|
actor_type: snapshot.reference.actor_type,
|
|
64
61
|
actor_id: snapshot.reference.actor_id,
|
|
65
62
|
message_name: observable_name,
|
|
66
|
-
arguments:
|
|
63
|
+
arguments: registration.authorization_arguments,
|
|
67
64
|
authorization_context:
|
|
68
65
|
)
|
|
69
66
|
return if authorized
|
|
@@ -76,7 +73,7 @@ module SolidObjects
|
|
|
76
73
|
actor_name = snapshot.actor_class.name
|
|
77
74
|
raise InvalidActor, "anonymous actors cannot render reactive components" unless actor_name
|
|
78
75
|
|
|
79
|
-
"actors/#{actor_name.underscore}/#{component_name}"
|
|
76
|
+
"actors/#{actor_name.underscore}/#{registration.component_name}"
|
|
80
77
|
end
|
|
81
78
|
end
|
|
82
79
|
end
|
|
@@ -28,7 +28,7 @@ module SolidObjects
|
|
|
28
28
|
validate_identity!(registration, reference)
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
|
-
if registrations.map(&:
|
|
31
|
+
if registrations.map(&:dom_id).uniq.length != registrations.length
|
|
32
32
|
raise InvalidComponentToken, "duplicate actor component registration"
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -39,7 +39,7 @@ module SolidObjects
|
|
|
39
39
|
def initialize(registrations)
|
|
40
40
|
@registrations = registrations
|
|
41
41
|
@revisions = registrations.to_h do |registration|
|
|
42
|
-
[ registration.
|
|
42
|
+
[ registration.dom_id, registration.revision_key ]
|
|
43
43
|
end
|
|
44
44
|
end
|
|
45
45
|
|
|
@@ -51,7 +51,7 @@ module SolidObjects
|
|
|
51
51
|
registrations.filter_map do |registration|
|
|
52
52
|
next unless registration.dependencies.include?(observable_name)
|
|
53
53
|
next unless newer_revision?(
|
|
54
|
-
registration.
|
|
54
|
+
registration.dom_id,
|
|
55
55
|
instance_id,
|
|
56
56
|
revision
|
|
57
57
|
)
|
|
@@ -64,7 +64,7 @@ module SolidObjects
|
|
|
64
64
|
def reconnect_refreshes(snapshot)
|
|
65
65
|
registrations.filter_map do |registration|
|
|
66
66
|
next unless newer_revision?(
|
|
67
|
-
registration.
|
|
67
|
+
registration.dom_id,
|
|
68
68
|
snapshot.instance_id,
|
|
69
69
|
snapshot.revision
|
|
70
70
|
)
|
|
@@ -92,7 +92,7 @@ module SolidObjects
|
|
|
92
92
|
|
|
93
93
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
94
94
|
def refresh(registration, instance_id, revision)
|
|
95
|
-
revisions[registration.
|
|
95
|
+
revisions[registration.dom_id] = [ instance_id, revision ]
|
|
96
96
|
TurboStreamRenderer.component_refresh(
|
|
97
97
|
registration,
|
|
98
98
|
instance_id,
|
|
@@ -101,8 +101,8 @@ module SolidObjects
|
|
|
101
101
|
end
|
|
102
102
|
|
|
103
103
|
# @rbs (String, Integer, Integer) -> bool
|
|
104
|
-
def newer_revision?(
|
|
105
|
-
current = revisions.fetch(
|
|
104
|
+
def newer_revision?(dom_id, instance_id, revision)
|
|
105
|
+
current = revisions.fetch(dom_id)
|
|
106
106
|
(current <=> [ instance_id, revision ]) == -1
|
|
107
107
|
end
|
|
108
108
|
end
|
|
@@ -7,23 +7,38 @@ module SolidObjects
|
|
|
7
7
|
PURPOSE = "solid_objects.actor_component"
|
|
8
8
|
MAXIMUM_TOKEN_BYTES = 16_384
|
|
9
9
|
MAXIMUM_DEPENDENCIES = 50
|
|
10
|
+
MAXIMUM_LOCALS = 50
|
|
11
|
+
MAXIMUM_COMPONENT_KEY_BYTES = 512
|
|
12
|
+
REFRESH_METHODS = %w[replace morph].freeze
|
|
13
|
+
RESERVED_LOCALS = %w[actor authorization_context component_key].freeze
|
|
14
|
+
RUBY_KEYWORDS = %w[
|
|
15
|
+
alias and begin break case class def defined do else elsif end ensure
|
|
16
|
+
false for if in module next nil not or redo rescue retry return self
|
|
17
|
+
super then true undef unless until when while yield
|
|
18
|
+
].freeze
|
|
10
19
|
|
|
11
20
|
module_function
|
|
12
21
|
|
|
13
|
-
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String) -> String
|
|
22
|
+
# @rbs (reference: Reference, component_name: String, dependencies: Array[String], instance_id: Integer, revision: Integer, refresh_path: String, ?component_key: untyped, ?locals: Hash[untyped, untyped], ?refresh_method: String | Symbol) -> String
|
|
14
23
|
def generate(
|
|
15
24
|
reference:,
|
|
16
25
|
component_name:,
|
|
17
26
|
dependencies:,
|
|
18
27
|
instance_id:,
|
|
19
28
|
revision:,
|
|
20
|
-
refresh_path
|
|
29
|
+
refresh_path:,
|
|
30
|
+
component_key: nil,
|
|
31
|
+
locals: {},
|
|
32
|
+
refresh_method: "replace"
|
|
21
33
|
)
|
|
22
34
|
payload = {
|
|
23
35
|
"actor_type" => reference.actor_type,
|
|
24
36
|
"actor_id" => reference.actor_id,
|
|
25
37
|
"component_name" => component_name,
|
|
38
|
+
"component_key" => Serialization.dump(component_key),
|
|
26
39
|
"dependencies" => dependencies,
|
|
40
|
+
"locals" => Serialization.dump(locals),
|
|
41
|
+
"refresh_method" => refresh_method.to_s,
|
|
27
42
|
"instance_id" => instance_id,
|
|
28
43
|
"revision" => revision,
|
|
29
44
|
"refresh_path" => refresh_path
|
|
@@ -41,6 +56,7 @@ module SolidObjects
|
|
|
41
56
|
end
|
|
42
57
|
|
|
43
58
|
payload = verifier.verified(token, purpose: PURPOSE)
|
|
59
|
+
apply_defaults!(payload)
|
|
44
60
|
validate_payload!(payload)
|
|
45
61
|
payload
|
|
46
62
|
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
@@ -54,6 +70,8 @@ module SolidObjects
|
|
|
54
70
|
payload["actor_id"].is_a?(String) &&
|
|
55
71
|
payload["component_name"].is_a?(String) &&
|
|
56
72
|
payload["dependencies"].is_a?(Array) &&
|
|
73
|
+
payload["locals"].is_a?(Hash) &&
|
|
74
|
+
payload["refresh_method"].is_a?(String) &&
|
|
57
75
|
payload["instance_id"].is_a?(Integer) &&
|
|
58
76
|
payload["revision"].is_a?(Integer) &&
|
|
59
77
|
payload["refresh_path"].is_a?(String)
|
|
@@ -61,7 +79,10 @@ module SolidObjects
|
|
|
61
79
|
end
|
|
62
80
|
|
|
63
81
|
validate_component_name!(payload.fetch("component_name"))
|
|
82
|
+
validate_component_key!(payload["component_key"])
|
|
64
83
|
validate_dependencies!(payload.fetch("dependencies"))
|
|
84
|
+
validate_locals!(payload.fetch("locals"))
|
|
85
|
+
validate_refresh_method!(payload.fetch("refresh_method"))
|
|
65
86
|
validate_revision!(
|
|
66
87
|
payload.fetch("instance_id"),
|
|
67
88
|
payload.fetch("revision")
|
|
@@ -71,6 +92,16 @@ module SolidObjects
|
|
|
71
92
|
end
|
|
72
93
|
private_class_method :validate_payload!
|
|
73
94
|
|
|
95
|
+
# @rbs (Hash[String, untyped]?) -> void
|
|
96
|
+
def apply_defaults!(payload)
|
|
97
|
+
return unless payload.is_a?(Hash)
|
|
98
|
+
|
|
99
|
+
payload["component_key"] = nil unless payload.key?("component_key")
|
|
100
|
+
payload["locals"] = {} unless payload.key?("locals")
|
|
101
|
+
payload["refresh_method"] = "replace" unless payload.key?("refresh_method")
|
|
102
|
+
end
|
|
103
|
+
private_class_method :apply_defaults!
|
|
104
|
+
|
|
74
105
|
# @rbs (String) -> void
|
|
75
106
|
def validate_component_name!(component_name)
|
|
76
107
|
return if component_name.match?(/\A[a-zA-Z0-9_]+\z/)
|
|
@@ -79,6 +110,20 @@ module SolidObjects
|
|
|
79
110
|
end
|
|
80
111
|
private_class_method :validate_component_name!
|
|
81
112
|
|
|
113
|
+
# @rbs (untyped) -> void
|
|
114
|
+
def validate_component_key!(component_key)
|
|
115
|
+
return if component_key.nil?
|
|
116
|
+
return if component_key.is_a?(Integer)
|
|
117
|
+
if component_key.is_a?(String) &&
|
|
118
|
+
component_key.bytesize.positive? &&
|
|
119
|
+
component_key.bytesize <= MAXIMUM_COMPONENT_KEY_BYTES
|
|
120
|
+
return
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
raise InvalidComponentToken, "invalid actor component key"
|
|
124
|
+
end
|
|
125
|
+
private_class_method :validate_component_key!
|
|
126
|
+
|
|
82
127
|
# @rbs (Array[untyped]) -> void
|
|
83
128
|
def validate_dependencies!(dependencies)
|
|
84
129
|
valid = dependencies.any? &&
|
|
@@ -91,6 +136,29 @@ module SolidObjects
|
|
|
91
136
|
end
|
|
92
137
|
private_class_method :validate_dependencies!
|
|
93
138
|
|
|
139
|
+
# @rbs (Hash[untyped, untyped]) -> void
|
|
140
|
+
def validate_locals!(locals)
|
|
141
|
+
valid = locals.length <= MAXIMUM_LOCALS &&
|
|
142
|
+
locals.keys.all? do |name|
|
|
143
|
+
name.is_a?(String) &&
|
|
144
|
+
name.match?(/\A[a-z_][a-zA-Z0-9_]*\z/) &&
|
|
145
|
+
!RESERVED_LOCALS.include?(name) &&
|
|
146
|
+
!RUBY_KEYWORDS.include?(name)
|
|
147
|
+
end
|
|
148
|
+
return if valid
|
|
149
|
+
|
|
150
|
+
raise InvalidComponentToken, "invalid actor component locals"
|
|
151
|
+
end
|
|
152
|
+
private_class_method :validate_locals!
|
|
153
|
+
|
|
154
|
+
# @rbs (String) -> void
|
|
155
|
+
def validate_refresh_method!(refresh_method)
|
|
156
|
+
return if REFRESH_METHODS.include?(refresh_method)
|
|
157
|
+
|
|
158
|
+
raise InvalidComponentToken, "invalid actor component refresh method"
|
|
159
|
+
end
|
|
160
|
+
private_class_method :validate_refresh_method!
|
|
161
|
+
|
|
94
162
|
# @rbs (Integer, Integer) -> void
|
|
95
163
|
def validate_revision!(instance_id, revision)
|
|
96
164
|
return unless instance_id.negative? || revision.negative?
|
|
@@ -52,6 +52,16 @@ module SolidObjects
|
|
|
52
52
|
value.is_a?(Time) ? value.utc : Time.parse("#{value} UTC").utc
|
|
53
53
|
end
|
|
54
54
|
|
|
55
|
+
# @rbs () { () -> untyped } -> untyped
|
|
56
|
+
def with_lock_retry
|
|
57
|
+
yield
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# @rbs () { () -> untyped } -> untyped
|
|
61
|
+
def with_lock_probe
|
|
62
|
+
yield
|
|
63
|
+
end
|
|
64
|
+
|
|
55
65
|
# @rbs () { () -> untyped } -> untyped
|
|
56
66
|
def transaction(&block)
|
|
57
67
|
raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
|
|
@@ -4,6 +4,8 @@ module SolidObjects
|
|
|
4
4
|
module DatabaseAdapters
|
|
5
5
|
class Sqlite < DatabaseAdapter
|
|
6
6
|
LOCK_RETRY_INTERVAL = 0.001
|
|
7
|
+
LOCK_RETRY_MUTEX = Thread::Mutex.new
|
|
8
|
+
LOCK_RETRY_CONDITION = Thread::ConditionVariable.new
|
|
7
9
|
|
|
8
10
|
# @rbs () -> String
|
|
9
11
|
def current_time_expression
|
|
@@ -14,14 +16,51 @@ module SolidObjects
|
|
|
14
16
|
def transaction(&block)
|
|
15
17
|
return super unless SyncDeadline.active?
|
|
16
18
|
|
|
17
|
-
super
|
|
19
|
+
with_lock_retry { super }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# @rbs () { () -> untyped } -> untyped
|
|
23
|
+
def with_lock_retry
|
|
24
|
+
return yield unless SyncDeadline.active?
|
|
25
|
+
|
|
26
|
+
raise DatabaseDeadlineExceeded, "synchronous invocation deadline expired" if SyncDeadline.expired?
|
|
27
|
+
|
|
28
|
+
with_connection do |connection|
|
|
29
|
+
with_transaction_deadline(connection) { yield }
|
|
30
|
+
end
|
|
18
31
|
rescue DatabaseDeadlineExceeded
|
|
19
32
|
raise if SyncDeadline.expired?
|
|
20
33
|
|
|
21
|
-
|
|
34
|
+
wait_before_retry
|
|
35
|
+
retry
|
|
36
|
+
rescue => error
|
|
37
|
+
raise unless deadline_error?(error)
|
|
38
|
+
|
|
39
|
+
if SyncDeadline.expired?
|
|
40
|
+
raise DatabaseDeadlineExceeded,
|
|
41
|
+
"database lock wait exceeded the synchronous invocation deadline",
|
|
42
|
+
cause: error
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
wait_before_retry
|
|
22
46
|
retry
|
|
23
47
|
end
|
|
24
48
|
|
|
49
|
+
# @rbs () { () -> untyped } -> untyped
|
|
50
|
+
def with_lock_probe
|
|
51
|
+
return yield unless SyncDeadline.active?
|
|
52
|
+
|
|
53
|
+
with_connection do |connection|
|
|
54
|
+
with_transaction_deadline(connection) { yield }
|
|
55
|
+
end
|
|
56
|
+
rescue => error
|
|
57
|
+
raise unless deadline_error?(error)
|
|
58
|
+
|
|
59
|
+
raise DatabaseDeadlineExceeded,
|
|
60
|
+
"database remained locked at the synchronous invocation deadline",
|
|
61
|
+
cause: error
|
|
62
|
+
end
|
|
63
|
+
|
|
25
64
|
private
|
|
26
65
|
|
|
27
66
|
# @rbs (untyped) { () -> untyped } -> untyped
|
|
@@ -47,6 +86,16 @@ module SolidObjects
|
|
|
47
86
|
end
|
|
48
87
|
false
|
|
49
88
|
end
|
|
89
|
+
|
|
90
|
+
# @rbs () -> void
|
|
91
|
+
def wait_before_retry
|
|
92
|
+
LOCK_RETRY_MUTEX.synchronize do
|
|
93
|
+
LOCK_RETRY_CONDITION.wait(
|
|
94
|
+
LOCK_RETRY_MUTEX,
|
|
95
|
+
[ LOCK_RETRY_INTERVAL, SyncDeadline.remaining ].min
|
|
96
|
+
)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
50
99
|
end
|
|
51
100
|
end
|
|
52
101
|
end
|
|
@@ -16,9 +16,12 @@ module SolidObjects
|
|
|
16
16
|
"#{scope(reference)}_observable_#{normalized_name(name)}"
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
# @rbs (Reference, Symbol | String) -> String
|
|
20
|
-
def component(reference, name)
|
|
21
|
-
"#{scope(reference)}_component_#{normalized_name(name)}"
|
|
19
|
+
# @rbs (Reference, Symbol | String, ?key: String | Integer?) -> String
|
|
20
|
+
def component(reference, name, key: nil)
|
|
21
|
+
identity = "#{scope(reference)}_component_#{normalized_name(name)}"
|
|
22
|
+
return identity unless key
|
|
23
|
+
|
|
24
|
+
"#{identity}_#{component_key_digest(key)}"
|
|
22
25
|
end
|
|
23
26
|
|
|
24
27
|
# @rbs (Reference) -> String
|
|
@@ -34,5 +37,11 @@ module SolidObjects
|
|
|
34
37
|
name.to_s.gsub(/[^a-zA-Z0-9_-]/, "_")
|
|
35
38
|
end
|
|
36
39
|
private_class_method :normalized_name
|
|
40
|
+
|
|
41
|
+
# @rbs (String | Integer) -> String
|
|
42
|
+
def component_key_digest(key)
|
|
43
|
+
Digest::SHA256.hexdigest(JSON.generate(key)).first(24)
|
|
44
|
+
end
|
|
45
|
+
private_class_method :component_key_digest
|
|
37
46
|
end
|
|
38
47
|
end
|
data/lib/solid_objects/engine.rb
CHANGED
|
@@ -26,6 +26,13 @@ module SolidObjects
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
+
initializer "solid_objects.assets" do |application|
|
|
30
|
+
next unless application.config.respond_to?(:assets)
|
|
31
|
+
next unless application.config.assets.respond_to?(:precompile)
|
|
32
|
+
|
|
33
|
+
application.config.assets.precompile << "solid_objects/component_refresh.js"
|
|
34
|
+
end
|
|
35
|
+
|
|
29
36
|
rake_tasks do
|
|
30
37
|
tasks_path = File.expand_path("../tasks", __dir__)
|
|
31
38
|
Dir[File.join(tasks_path, "**/*.rake")].sort.each { |task| load task }
|
|
@@ -80,18 +80,21 @@ module SolidObjects
|
|
|
80
80
|
|
|
81
81
|
# @rbs (?kind: String, ?metadata: Hash[String | Symbol, untyped]) -> Process
|
|
82
82
|
def register(kind: "worker", metadata: {})
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
83
|
+
process_record = SolidObjects.database_adapter.with_lock_retry do
|
|
84
|
+
now = SolidObjects.database_adapter.database_now
|
|
85
|
+
Process.create!(
|
|
86
|
+
id: SecureRandom.uuid,
|
|
87
|
+
kind:,
|
|
88
|
+
hostname: Socket.gethostname,
|
|
89
|
+
pid: ::Process.pid,
|
|
90
|
+
started_at: now,
|
|
91
|
+
last_heartbeat_at: now,
|
|
92
|
+
metadata: Serialization.dump(default_metadata.merge(metadata))
|
|
93
|
+
)
|
|
94
|
+
end
|
|
95
|
+
@process_record = process_record
|
|
93
96
|
@last_heartbeat_at = monotonic_now
|
|
94
|
-
|
|
97
|
+
process_record
|
|
95
98
|
end
|
|
96
99
|
|
|
97
100
|
# @rbs () -> bool
|
|
@@ -99,9 +102,13 @@ module SolidObjects
|
|
|
99
102
|
return false unless process_record
|
|
100
103
|
return false if heartbeat_recent?
|
|
101
104
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
+
updated = SolidObjects.database_adapter.with_lock_retry do
|
|
106
|
+
process_record.update(
|
|
107
|
+
last_heartbeat_at: SolidObjects.database_adapter.database_now
|
|
108
|
+
)
|
|
109
|
+
end
|
|
110
|
+
@last_heartbeat_at = monotonic_now if updated
|
|
111
|
+
updated
|
|
105
112
|
end
|
|
106
113
|
|
|
107
114
|
# @rbs () -> bool
|
|
@@ -10,6 +10,62 @@ module SolidObjects
|
|
|
10
10
|
status = message_status(message)
|
|
11
11
|
waiting_on = waiting_reason(message, instance, blocker)
|
|
12
12
|
activation = activation_details(instance)
|
|
13
|
+
build_error(
|
|
14
|
+
message,
|
|
15
|
+
timeout:,
|
|
16
|
+
status:,
|
|
17
|
+
waiting_on:,
|
|
18
|
+
activation:,
|
|
19
|
+
blocker: blocker_details(blocker)
|
|
20
|
+
)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
24
|
+
def database_contention(message, timeout:)
|
|
25
|
+
build_error(
|
|
26
|
+
message,
|
|
27
|
+
timeout:,
|
|
28
|
+
status: "unknown",
|
|
29
|
+
waiting_on: "database_contention",
|
|
30
|
+
activation: {},
|
|
31
|
+
blocker: nil
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# @rbs (MessageReference, timeout: Numeric) -> SyncTimeout
|
|
36
|
+
def database_contention_for(message_reference, timeout:)
|
|
37
|
+
error = SyncTimeout.new(
|
|
38
|
+
timeout:,
|
|
39
|
+
actor_type: message_reference.actor_type,
|
|
40
|
+
actor_id: message_reference.actor_id,
|
|
41
|
+
message_name: "unknown",
|
|
42
|
+
message_id: message_reference.id,
|
|
43
|
+
request_id: message_reference.request_id,
|
|
44
|
+
sequence: message_reference.sequence,
|
|
45
|
+
status: "unknown",
|
|
46
|
+
waiting_on: "database_contention",
|
|
47
|
+
activation: {},
|
|
48
|
+
blocker: nil
|
|
49
|
+
)
|
|
50
|
+
SolidObjects.instrument(
|
|
51
|
+
:"sync.timeout",
|
|
52
|
+
message_id: message_reference.id,
|
|
53
|
+
request_id: message_reference.request_id,
|
|
54
|
+
actor_type: message_reference.actor_type,
|
|
55
|
+
actor_id: message_reference.actor_id,
|
|
56
|
+
sequence: message_reference.sequence,
|
|
57
|
+
status: "unknown",
|
|
58
|
+
waiting_on: "database_contention",
|
|
59
|
+
activation_owner_id: nil,
|
|
60
|
+
activation_generation: nil
|
|
61
|
+
)
|
|
62
|
+
error
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
private
|
|
66
|
+
|
|
67
|
+
# @rbs (Message, timeout: Numeric, status: String, waiting_on: String, activation: Hash[String, untyped], blocker: Hash[String, untyped]?) -> SyncTimeout
|
|
68
|
+
def build_error(message, timeout:, status:, waiting_on:, activation:, blocker:)
|
|
13
69
|
error = SyncTimeout.new(
|
|
14
70
|
timeout:,
|
|
15
71
|
actor_type: message.actor_type,
|
|
@@ -21,7 +77,7 @@ module SolidObjects
|
|
|
21
77
|
status:,
|
|
22
78
|
waiting_on:,
|
|
23
79
|
activation:,
|
|
24
|
-
blocker:
|
|
80
|
+
blocker:
|
|
25
81
|
)
|
|
26
82
|
SolidObjects.instrument(
|
|
27
83
|
:"sync.timeout",
|
|
@@ -38,8 +94,6 @@ module SolidObjects
|
|
|
38
94
|
error
|
|
39
95
|
end
|
|
40
96
|
|
|
41
|
-
private
|
|
42
|
-
|
|
43
97
|
# @rbs (Message) -> String
|
|
44
98
|
def message_status(message)
|
|
45
99
|
return "rejected" if message.rejected?
|
|
@@ -22,33 +22,49 @@ module SolidObjects
|
|
|
22
22
|
# @rbs (MessageReference, timeout: Numeric) -> untyped
|
|
23
23
|
def call_before_deadline(message_reference, timeout:)
|
|
24
24
|
deadline = monotonic_now + SyncDeadline.remaining
|
|
25
|
+
message = nil
|
|
25
26
|
|
|
26
27
|
loop do
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
remaining = deadline - monotonic_now
|
|
31
|
-
unless remaining.positive?
|
|
32
|
-
message = load_message(message_reference)
|
|
33
|
-
return completed_result(message) if message.completed? || message.dead?
|
|
28
|
+
unless (deadline - monotonic_now).positive?
|
|
29
|
+
final_message = load_message_at_deadline(message_reference)
|
|
30
|
+
return completed_result(final_message) if final_message&.completed? || final_message&.dead?
|
|
34
31
|
|
|
35
|
-
raise
|
|
32
|
+
raise final_message ?
|
|
33
|
+
diagnose_timeout(final_message, timeout:) :
|
|
34
|
+
contention_timeout(message, message_reference, timeout:)
|
|
36
35
|
end
|
|
37
36
|
|
|
37
|
+
message = load_message(message_reference)
|
|
38
|
+
return completed_result(message) if message.completed? || message.dead?
|
|
39
|
+
|
|
38
40
|
processed = assist(message, deadline:)
|
|
39
41
|
remaining = deadline - monotonic_now
|
|
40
42
|
wait(remaining) if processed.zero? && remaining.positive?
|
|
41
43
|
end
|
|
42
44
|
rescue DatabaseDeadlineExceeded
|
|
43
|
-
message
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
raise SyncDiagnostics.new.call(message, timeout:)
|
|
45
|
+
raise message ?
|
|
46
|
+
diagnose_timeout(message, timeout:) :
|
|
47
|
+
contention_timeout(message, message_reference, timeout:)
|
|
47
48
|
end
|
|
48
49
|
|
|
49
50
|
# @rbs (MessageReference) -> Message
|
|
50
51
|
def load_message(message_reference)
|
|
51
|
-
|
|
52
|
+
SolidObjects.database_adapter.with_lock_retry do
|
|
53
|
+
Message.uncached do
|
|
54
|
+
Message.includes(:dead_letter).find(message_reference.id)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# @rbs (MessageReference) -> Message?
|
|
60
|
+
def load_message_at_deadline(message_reference)
|
|
61
|
+
SolidObjects.database_adapter.with_lock_probe do
|
|
62
|
+
Message.uncached do
|
|
63
|
+
Message.includes(:dead_letter).find(message_reference.id)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
rescue DatabaseDeadlineExceeded
|
|
67
|
+
nil
|
|
52
68
|
end
|
|
53
69
|
|
|
54
70
|
# @rbs (Message) -> untyped
|
|
@@ -104,6 +120,22 @@ module SolidObjects
|
|
|
104
120
|
)
|
|
105
121
|
end
|
|
106
122
|
|
|
123
|
+
# @rbs (Message, timeout: Numeric) -> SyncTimeout
|
|
124
|
+
def diagnose_timeout(message, timeout:)
|
|
125
|
+
SolidObjects.database_adapter.with_lock_probe do
|
|
126
|
+
SyncDiagnostics.new.call(message, timeout:)
|
|
127
|
+
end
|
|
128
|
+
rescue DatabaseDeadlineExceeded
|
|
129
|
+
SyncDiagnostics.new.database_contention(message, timeout:)
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# @rbs (Message?, MessageReference, timeout: Numeric) -> SyncTimeout
|
|
133
|
+
def contention_timeout(message, message_reference, timeout:)
|
|
134
|
+
return SyncDiagnostics.new.database_contention(message, timeout:) if message
|
|
135
|
+
|
|
136
|
+
SyncDiagnostics.new.database_contention_for(message_reference, timeout:)
|
|
137
|
+
end
|
|
138
|
+
|
|
107
139
|
# @rbs () -> Float
|
|
108
140
|
def monotonic_now
|
|
109
141
|
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
@@ -40,15 +40,14 @@ module SolidObjects
|
|
|
40
40
|
|
|
41
41
|
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
42
42
|
def component_refresh(registration, instance_id, revision)
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
)
|
|
43
|
+
return morph_component_refresh(registration, instance_id, revision) if registration.morph?
|
|
44
|
+
|
|
45
|
+
target = registration.dom_id
|
|
47
46
|
source = ERB::Util.html_escape(
|
|
48
47
|
registration.refresh_url(instance_id, revision)
|
|
49
48
|
)
|
|
50
49
|
revision_value = "#{instance_id}:#{revision}"
|
|
51
|
-
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}"></turbo-frame></template></turbo-stream>)
|
|
50
|
+
%(<turbo-stream action="replace" target="#{target}"><template><turbo-frame id="#{target}" src="#{source}" data-solid-objects-revision="#{revision_value}" data-solid-objects-refresh="replace"></turbo-frame></template></turbo-stream>)
|
|
52
51
|
end
|
|
53
52
|
|
|
54
53
|
# @rbs (String) -> Hash[String, untyped]?
|
|
@@ -75,5 +74,15 @@ module SolidObjects
|
|
|
75
74
|
JSON.generate(value)
|
|
76
75
|
end
|
|
77
76
|
private_class_method :display_value
|
|
77
|
+
|
|
78
|
+
# @rbs (ComponentRegistration, Integer, Integer) -> String
|
|
79
|
+
def morph_component_refresh(registration, instance_id, revision)
|
|
80
|
+
source = ERB::Util.html_escape(
|
|
81
|
+
registration.refresh_url(instance_id, revision)
|
|
82
|
+
)
|
|
83
|
+
scope = DomIdentity.scope(registration.reference)
|
|
84
|
+
%(<turbo-stream action="append" target="#{scope}"><template><solid-objects-refresh data-target="#{registration.dom_id}" data-source="#{source}" data-refresh-method="morph"></solid-objects-refresh></template></turbo-stream>)
|
|
85
|
+
end
|
|
86
|
+
private_class_method :morph_component_refresh
|
|
78
87
|
end
|
|
79
88
|
end
|