solid_objects 0.14.3 → 0.14.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0c919699e07e7e1bd517a9fe0fa748558262c0cfe8483bcb992f8b85f65cb857
4
- data.tar.gz: 30a29702722638f5168664ecbb69f174564419f7308cc7596d71e9066baa2e76
3
+ metadata.gz: 32207ee5017c24fa87258061048c532532bcf599d46357ce8da4deef8cb0c93e
4
+ data.tar.gz: d5594c50173f64f96303e91b0bc0b0659335d42b0d16b4df1e6f618ad98041fc
5
5
  SHA512:
6
- metadata.gz: b8eee4e1bd40b39d67c3a74b2860a0f874336285d7291dcc740b578f8b4a8fedd3d1323e716c47df71f3615faef832519cbd3541f505f9e0c00b413f01a9e9e5
7
- data.tar.gz: 2b808b25de0a30d05da80b24451f1c12139c3be6bf90f4fb760f754d42b775e22cb60339e75ec8c750f341f89e6f50cbb05f809d6cea936070183a0b2cb8a3e8
6
+ metadata.gz: 6578fe32fed59a2c8fc6f4209244de1d33793561af20b258d96c8bd7c5cd78e7203fa08ebaea4a2c8315c89980bf4033852d00c3f4cadad3a6986c766dea7dad
7
+ data.tar.gz: ec2db7a0481f6075ef9e1565a47f972bc41f6d464b65a7700139b4209636e079a254454007857ce7988fb1d01b745277a71a737d8f3e49f340e768106b511c38
data/CHANGELOG.md CHANGED
@@ -1,5 +1,25 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.14.4 - 2026-08-30
4
+
5
+ - Reuse the encoding the after image already built. `State#to_h` copies the
6
+ state by encoding it and parsing the result, then threw the encoded string
7
+ away, and `complete` normalized and encoded the same hash a second time to
8
+ measure it. `to_h_with_byte_size` returns the copy with the size of the
9
+ encoding that produced it, so a committed turn now traverses the state twice
10
+ rather than three times and encodes it twice rather than three times.
11
+ Measured on SQLite against 0.14.3, committed throughput rises 5.3% at 13 KB
12
+ of state, 16.1% at 116 KB, and 12.0% at 1 MB. `docs/benchmarks.md` holds the
13
+ numbers. `solid-objects-js` already measured the string it commits; this
14
+ brings the gem to the same shape.
15
+ - Add `Serialization.deep_copy_with_byte_size`, which returns a deep copy
16
+ beside the size of its encoded form. `deep_copy` now calls it, so both
17
+ encode once.
18
+ - Enforce `max_state_bytes` against the size the after image reports rather
19
+ than by encoding the state again. The turn still fails with
20
+ `PayloadTooLarge` before it opens its commit transaction, which a test now
21
+ covers end to end.
22
+
3
23
  ## 0.14.3 - 2026-08-29
4
24
 
5
25
  - Cut one of the three full state copies a committed turn made. The executor
data/docs/benchmarks.md CHANGED
@@ -142,6 +142,22 @@ used to do, so part of the saving pays for that check. The gain grows with the
142
142
  state, because the database write dominates a small turn. The empty-state row
143
143
  sits inside run-to-run variance.
144
144
 
145
+ `0.14.4` removed the traversal that remained. Copying the state encodes it and
146
+ parses the result, and the commit then normalized and encoded the same hash
147
+ again to measure it. The copy now reports the size of the encoding that
148
+ produced it. Measured the same way, against the released `0.14.3` tree:
149
+
150
+ | Committed state | 0.14.3 | 0.14.4 | Change |
151
+ | ---: | ---: | ---: | ---: |
152
+ | 23 bytes | 1,251.2 messages/s | 1,242.8 messages/s | -0.7% |
153
+ | 13,662 bytes | 612.0 messages/s | 644.7 messages/s | +5.3% |
154
+ | 118,786 bytes | 171.9 messages/s | 199.6 messages/s | +16.1% |
155
+ | 1,026,356 bytes | 23.4 messages/s | 26.2 messages/s | +12.0% |
156
+
157
+ A committed turn now traverses the state twice and encodes it twice, against
158
+ three of each before. The empty-state row again sits inside run-to-run
159
+ variance.
160
+
145
161
  The curve matters more than the change. Throughput falls about 28 times between
146
162
  13 KB and 1 MB of state, and about 53 times between an empty state and 1 MB.
147
163
  The `max_state_bytes` default of 5 MB is therefore a limit rather than an
@@ -26,13 +26,13 @@ module SolidObjects
26
26
  SolidObjects.instrument(:"message.started", **instrumentation_payload)
27
27
  result = invoke_actor(message_context)
28
28
  observable_changes = changed_observables(observables_before, actor.observable_values)
29
- state_after = actor.state.to_h
30
- ensure_query_did_not_mutate_state!(state_before, state_after)
29
+ state_after = actor.state.to_h_with_byte_size
30
+ ensure_query_did_not_mutate_state!(state_before, state_after.value)
31
31
  complete(
32
32
  result,
33
33
  observable_changes,
34
34
  state_after:,
35
- state_changed: state_after != state_before
35
+ state_changed: state_after.value != state_before
36
36
  )
37
37
  true
38
38
  rescue LostActivation
@@ -81,12 +81,9 @@ module SolidObjects
81
81
  end
82
82
  end
83
83
 
84
- # @rbs (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
84
+ # @rbs (untyped, Hash[String, untyped], state_after: Serialization::Dumped, state_changed: bool) -> void
85
85
  def complete(result, observable_changes, state_after:, state_changed:)
86
- dumped_state = Serialization.dump_with_byte_size(
87
- state_after,
88
- max_bytes: SolidObjects.configuration.max_state_bytes
89
- )
86
+ ensure_state_fits!(state_after.byte_size)
90
87
  serialized_result = Serialization.dump(
91
88
  (message.delivery_mode == "sync") ? result : nil,
92
89
  max_bytes: SolidObjects.configuration.max_result_bytes
@@ -108,7 +105,7 @@ module SolidObjects
108
105
  locked_message = Message.lock.find(message.id)
109
106
  execute_commit_actions(commit_action_intents)
110
107
  instance.update!(
111
- state: dumped_state.value,
108
+ state: state_after.value,
112
109
  state_version: actor.class.state_version,
113
110
  state_revision: locked_message.sequence,
114
111
  last_used_at: SolidObjects.database_adapter.database_now
@@ -154,11 +151,19 @@ module SolidObjects
154
151
  actor_id: message.actor_id
155
152
  )
156
153
  end
157
- report_large_state(dumped_state.byte_size)
154
+ report_large_state(state_after.byte_size)
158
155
  SolidObjects.instrument_after_commit(:"message.completed", **instrumentation_payload)
159
156
  SolidObjects.wake_up.signal
160
157
  end
161
158
 
159
+ # @rbs (Integer) -> void
160
+ def ensure_state_fits!(byte_size)
161
+ max_bytes = SolidObjects.configuration.max_state_bytes
162
+ return if byte_size <= max_bytes
163
+
164
+ raise PayloadTooLarge, "serialized value exceeds #{max_bytes} bytes"
165
+ end
166
+
162
167
  # @rbs (Integer) -> void
163
168
  def report_large_state(byte_count)
164
169
  threshold = SolidObjects.configuration.warn_state_bytes
@@ -37,7 +37,14 @@ module SolidObjects
37
37
 
38
38
  # @rbs (untyped) -> untyped
39
39
  def deep_copy(value)
40
- JSON.parse(JSON.generate(normalize(value), max_nesting: MAX_NESTING))
40
+ deep_copy_with_byte_size(value).value
41
+ end
42
+
43
+ # @rbs (untyped) -> Dumped
44
+ def deep_copy_with_byte_size(value)
45
+ encoded = JSON.generate(normalize(value), max_nesting: MAX_NESTING)
46
+
47
+ Dumped.new(value: JSON.parse(encoded), byte_size: encoded.bytesize)
41
48
  rescue JSON::GeneratorError, JSON::ParserError, EncodingError => error
42
49
  raise InvalidPayload, error.message
43
50
  end
@@ -52,6 +52,11 @@ module SolidObjects
52
52
  Serialization.deep_copy(data)
53
53
  end
54
54
 
55
+ # @rbs () -> Serialization::Dumped
56
+ def to_h_with_byte_size
57
+ Serialization.deep_copy_with_byte_size(data)
58
+ end
59
+
55
60
  # @rbs (Symbol | String) -> untyped
56
61
  def fetch(name)
57
62
  key = name.to_s
@@ -1,5 +1,5 @@
1
1
  # rbs_inline: enabled
2
2
 
3
3
  module SolidObjects
4
- VERSION = "0.14.3"
4
+ VERSION = "0.14.4"
5
5
  end
@@ -30,8 +30,11 @@ module SolidObjects
30
30
  # @rbs (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
31
31
  def changed_observables: (Hash[String, untyped], Hash[String, untyped]) -> Hash[String, untyped]
32
32
 
33
- # @rbs (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
34
- def complete: (untyped, Hash[String, untyped], state_after: Hash[String, untyped], state_changed: bool) -> void
33
+ # @rbs (untyped, Hash[String, untyped], state_after: Serialization::Dumped, state_changed: bool) -> void
34
+ def complete: (untyped, Hash[String, untyped], state_after: Serialization::Dumped, state_changed: bool) -> void
35
+
36
+ # @rbs (Integer) -> void
37
+ def ensure_state_fits!: (Integer) -> void
35
38
 
36
39
  # @rbs (Integer) -> void
37
40
  def report_large_state: (Integer) -> void
@@ -29,6 +29,9 @@ module SolidObjects
29
29
  # @rbs (untyped) -> untyped
30
30
  def self.deep_copy: (untyped) -> untyped
31
31
 
32
+ # @rbs (untyped) -> Dumped
33
+ def self.deep_copy_with_byte_size: (untyped) -> Dumped
34
+
32
35
  # @rbs (untyped) -> untyped
33
36
  def self.readonly_copy: (untyped) -> untyped
34
37
 
@@ -45,6 +45,9 @@ module SolidObjects
45
45
  # @rbs () -> Hash[String, untyped]
46
46
  def to_h: () -> Hash[String, untyped]
47
47
 
48
+ # @rbs () -> Serialization::Dumped
49
+ def to_h_with_byte_size: () -> Serialization::Dumped
50
+
48
51
  # @rbs (Symbol | String) -> untyped
49
52
  def fetch: (Symbol | String) -> untyped
50
53
 
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_objects
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.14.3
4
+ version: 0.14.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Lucas Carlson
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-29 00:00:00.000000000 Z
11
+ date: 2026-08-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: actioncable