hecks 3.4.0 → 3.4.1
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/lib/hecks/adapters/driven/memory/shared_elements.rb +137 -0
- data/lib/hecks/adapters/driven/memory.rb +12 -4
- data/lib/hecks/ports/persistence/state_codec.rb +13 -0
- data/lib/hecks/runtime/instance.rb +5 -2
- data/lib/hecks/version.rb +1 -1
- data/rust/host/HECKS_RELEASE +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: b47e9f61f073e7c8512365b3bb5112f4e214c26cec46e08a323c6f02c509d555
|
|
4
|
+
data.tar.gz: d84b642199556cc1b9bbcbc6635c2e248f3f4b63232bb0ca6abee589f85661ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e2280fdb747d6d8a299bd58f4ab261e71a0d977ed805e9fcfe86ae108cc248724c96960614b9af6796d4617ef9102be1343107a91c6044123a97f298ba6002f1
|
|
7
|
+
data.tar.gz: 6558a4e9698ca6e01a665ab3d9aa435da6990e20a69ef06b0c0cd91bc0edcedc771a976dc664c4501d56883ebca1435adb534d3516a20d8fd55dca3c90ff40ff
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
require "objspace"
|
|
2
|
+
require_relative "../../../freezer"
|
|
3
|
+
require_relative "../../../ports/persistence/state_codec"
|
|
4
|
+
require_relative "../../../runtime/value"
|
|
5
|
+
|
|
6
|
+
module Hecks
|
|
7
|
+
module Adapters
|
|
8
|
+
class Memory
|
|
9
|
+
# Lets a Memory save of a growing `list_of` copy and hydrate only the elements it has not
|
|
10
|
+
# seen. A hydrated composite element is frozen, so its codec copy (the journal form) and
|
|
11
|
+
# its hydrated form are each computed once, frozen, and shared by every journal entry and
|
|
12
|
+
# record that holds it. Shared nodes are immutable, so a caller cannot reach the journal
|
|
13
|
+
# through a returned instance. Everything else in the state goes through `StateCodec`
|
|
14
|
+
# whole. Both tables are weak and identity-keyed: a cache, never the only holder.
|
|
15
|
+
class SharedElements
|
|
16
|
+
# @param aggregate [Bluebook::Aggregate] the aggregate whose state this shares elements of
|
|
17
|
+
def initialize(aggregate)
|
|
18
|
+
@aggregate = aggregate
|
|
19
|
+
reset!
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Forgets every remembered element.
|
|
23
|
+
#
|
|
24
|
+
# @return [SharedElements] self, now empty
|
|
25
|
+
def reset!
|
|
26
|
+
@journal = ObjectSpace::WeakMap.new # live element => frozen decoded copy
|
|
27
|
+
@live = ObjectSpace::WeakMap.new # frozen decoded copy => hydrated element
|
|
28
|
+
self
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Copies state into the shape the journal holds: `StateCodec.copy`, with every
|
|
32
|
+
# shareable list element copied once and reused.
|
|
33
|
+
#
|
|
34
|
+
# @param state [Hash, nil] live state, such as `Instance#state`; nil for a delete entry
|
|
35
|
+
# @return [Hash{Symbol => Object}, nil] the decoded copy; its shareable list elements
|
|
36
|
+
# are frozen and shared with every other copy that holds them, nothing else is shared
|
|
37
|
+
# with `state`
|
|
38
|
+
def journal_state(state)
|
|
39
|
+
lists = shareable_lists(state)
|
|
40
|
+
return Ports::Persistence::StateCodec.copy(@aggregate, state) if lists.empty?
|
|
41
|
+
|
|
42
|
+
copied = Ports::Persistence::StateCodec.copy(@aggregate, placeholders(state, lists))
|
|
43
|
+
lists.each { |name, (attribute, elements)| copied[name] = journal_list(attribute, elements) }
|
|
44
|
+
copied
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Hydrates journal-shaped state into the state a record holds.
|
|
48
|
+
#
|
|
49
|
+
# @param decoded [Hash] a fresh state in the shape `journal_state` returns; its non-shared
|
|
50
|
+
# values are hydrated in place of copies, so it must not be a journal entry's own state
|
|
51
|
+
# @return [Hash{Symbol => Object}] hydrated state, defaults filled; the lists it shares
|
|
52
|
+
# are frozen arrays of frozen elements, and nothing mutable is shared with `decoded`
|
|
53
|
+
def live_state(decoded)
|
|
54
|
+
lists = shareable_lists(decoded)
|
|
55
|
+
hydrated = Runtime::Instance.hydrate_with_defaults(@aggregate, placeholders(decoded, lists))
|
|
56
|
+
lists.each { |name, (attribute, elements)| hydrated[name] = live_list(attribute, elements) }
|
|
57
|
+
hydrated
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
# The list attributes of `state` whose elements can be shared: composite, not a
|
|
63
|
+
# reference, and actually held as an Array.
|
|
64
|
+
def shareable_lists(state)
|
|
65
|
+
return {} unless state.is_a?(Hash)
|
|
66
|
+
|
|
67
|
+
composite_lists.each_with_object({}) do |(name, attribute), found|
|
|
68
|
+
elements = state[name]
|
|
69
|
+
found[name] = [attribute, elements] if elements.is_a?(Array)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def composite_lists
|
|
74
|
+
@composite_lists ||= @aggregate.attributes.each_with_object({}) do |attribute, found|
|
|
75
|
+
next unless attribute.list? && !attribute.reference? && composite?(attribute)
|
|
76
|
+
|
|
77
|
+
found[attribute.name.to_sym] = attribute
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def composite?(attribute)
|
|
82
|
+
type = attribute.type.to_s
|
|
83
|
+
!Runtime::Value.find_entity(@aggregate, type).nil? ||
|
|
84
|
+
(@aggregate.respond_to?(:value_object) && !Runtime::Value.value_object_for(@aggregate, type).nil?)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# `state` with each shared list replaced by nil, which the codec leaves alone and
|
|
88
|
+
# which holds the list's place in the key order.
|
|
89
|
+
def placeholders(state, lists)
|
|
90
|
+
return state if lists.empty?
|
|
91
|
+
|
|
92
|
+
state.merge(lists.transform_values { nil })
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def journal_list(attribute, elements)
|
|
96
|
+
elements.map { |element| journal_element(attribute, element) }.freeze
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def journal_element(attribute, element)
|
|
100
|
+
known = @journal[element]
|
|
101
|
+
return known if known
|
|
102
|
+
return element if @live.key?(element)
|
|
103
|
+
|
|
104
|
+
copy = Ports::Persistence::StateCodec.copy_list_element(@aggregate, attribute, element)
|
|
105
|
+
return copy unless immutable?(element)
|
|
106
|
+
|
|
107
|
+
@journal[element] = Freezer.deep(copy)
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def live_list(attribute, elements)
|
|
111
|
+
elements.map { |element| live_element(attribute, element) }.freeze
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def live_element(attribute, element)
|
|
115
|
+
known = @live[element]
|
|
116
|
+
return known if known
|
|
117
|
+
|
|
118
|
+
live = Runtime::Value.trusting_stored_state do
|
|
119
|
+
Runtime::Value.hydrate_entity_list(@aggregate, attribute, [element]).first
|
|
120
|
+
end
|
|
121
|
+
return live unless element.is_a?(Hash) && immutable?(element)
|
|
122
|
+
|
|
123
|
+
@journal[live] = element
|
|
124
|
+
@live[element] = live
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def immutable?(element)
|
|
128
|
+
case element
|
|
129
|
+
when Runtime::Value then element.frozen?
|
|
130
|
+
when Hash then Freezer.deeply_frozen?(element)
|
|
131
|
+
else false
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -2,6 +2,7 @@ require_relative "../../ports/persistence/append_only"
|
|
|
2
2
|
require_relative "../../ports/query/in_memory"
|
|
3
3
|
require_relative "in_memory_ordering"
|
|
4
4
|
require_relative "../../runtime/instance"
|
|
5
|
+
require_relative "memory/shared_elements"
|
|
5
6
|
|
|
6
7
|
module Hecks
|
|
7
8
|
module Adapters
|
|
@@ -30,6 +31,7 @@ module Hecks
|
|
|
30
31
|
# constructor shape and ignored
|
|
31
32
|
def initialize(aggregate:, settings: {}, root: nil)
|
|
32
33
|
@aggregate = aggregate
|
|
34
|
+
@elements = SharedElements.new(aggregate)
|
|
33
35
|
@records = {}
|
|
34
36
|
@events = []
|
|
35
37
|
@entries = []
|
|
@@ -105,7 +107,7 @@ module Hecks
|
|
|
105
107
|
# @param instance [Runtime::Instance] the instance to store
|
|
106
108
|
# @return [Runtime::Instance] the stored record, a fresh instance over a copy of the state
|
|
107
109
|
def save(instance)
|
|
108
|
-
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state:
|
|
110
|
+
entry = Ports::Persistence::Entry.new(operation: "save", id: instance.id.to_s, state: instance.state)
|
|
109
111
|
append(entry)
|
|
110
112
|
project(entry)
|
|
111
113
|
end
|
|
@@ -164,6 +166,7 @@ module Hecks
|
|
|
164
166
|
@entries = []
|
|
165
167
|
@outbox = []
|
|
166
168
|
@outbox_deliveries = {}
|
|
169
|
+
@elements.reset!
|
|
167
170
|
self
|
|
168
171
|
end
|
|
169
172
|
|
|
@@ -260,15 +263,20 @@ module Hecks
|
|
|
260
263
|
def copy(state)
|
|
261
264
|
return state.dup if bootstrap_fast_path?(state)
|
|
262
265
|
|
|
263
|
-
|
|
266
|
+
@elements.journal_state(state)
|
|
264
267
|
end
|
|
265
268
|
|
|
266
269
|
def build_instance(entry)
|
|
267
270
|
if bootstrap_fast_path?(entry.state)
|
|
268
271
|
Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: entry.state.dup, hydrate: false)
|
|
269
272
|
else
|
|
270
|
-
decoded =
|
|
271
|
-
|
|
273
|
+
decoded = @elements.journal_state(entry.state)
|
|
274
|
+
# The state is decoded by construction, so the boundary's re-walk of every shared
|
|
275
|
+
# list element would only repeat the copy that just built it.
|
|
276
|
+
Ports::Persistence::CodecBoundary.outside do
|
|
277
|
+
Runtime::Instance.new(aggregate: @aggregate, id: entry.id, state: decoded,
|
|
278
|
+
hydrate_with: @elements.method(:live_state))
|
|
279
|
+
end
|
|
272
280
|
end
|
|
273
281
|
end
|
|
274
282
|
end
|
|
@@ -46,6 +46,19 @@ module Hecks
|
|
|
46
46
|
# Array with `state`; nil when `state` is nil
|
|
47
47
|
def copy(aggregate, state) = decode(aggregate, encode(aggregate, state))
|
|
48
48
|
|
|
49
|
+
# Deep-copies one element of a `list_of` composite attribute, exactly as `copy` would
|
|
50
|
+
# copy it inside its list.
|
|
51
|
+
#
|
|
52
|
+
# @param aggregate [Bluebook::Aggregate, Bluebook::Entity] the construct that declares
|
|
53
|
+
# the list attribute
|
|
54
|
+
# @param attribute [Bluebook::Attribute] a list attribute of `aggregate`
|
|
55
|
+
# @param element [Hash, Runtime::Value, Object] one element of the live list
|
|
56
|
+
# @return [Hash, Object] a new decoded Hash sharing nothing with `element`; a leaf
|
|
57
|
+
# element is returned as its JSON round trip
|
|
58
|
+
def copy_list_element(aggregate, attribute, element)
|
|
59
|
+
decode_composite(aggregate, attribute.type.to_s, encode_value(element))
|
|
60
|
+
end
|
|
61
|
+
|
|
49
62
|
# Maps every declared top-level field name to its attribute.
|
|
50
63
|
#
|
|
51
64
|
# An attribute that shares a name with the lifecycle field or a projected field keeps
|
|
@@ -16,7 +16,10 @@ module Hecks
|
|
|
16
16
|
# `args` is the creation payload, used only to fill a composite identity's heads.
|
|
17
17
|
# `hydrate: false` skips the default re-walk for an already-hydrated, validated state,
|
|
18
18
|
# keeping a `list_of` entity's Nth save O(1) rather than O(N).
|
|
19
|
-
|
|
19
|
+
# `hydrate_with:` replaces the default hydration with a callable that takes the given
|
|
20
|
+
# state and answers the hydrated one (defaults filled), for an adapter that can reuse
|
|
21
|
+
# work across saves.
|
|
22
|
+
def initialize(aggregate:, id:, state: nil, args: nil, hydrate: true, hydrate_with: nil)
|
|
20
23
|
@aggregate = aggregate
|
|
21
24
|
@id = id
|
|
22
25
|
# Inside a persistence adapter call this refuses undecoded stored state.
|
|
@@ -24,7 +27,7 @@ module Hecks
|
|
|
24
27
|
@state = if !hydrate
|
|
25
28
|
state || self.class.defaults(aggregate)
|
|
26
29
|
elsif state
|
|
27
|
-
self.class.hydrate_with_defaults(aggregate, state)
|
|
30
|
+
hydrate_with ? hydrate_with.call(state) : self.class.hydrate_with_defaults(aggregate, state)
|
|
28
31
|
else
|
|
29
32
|
self.class.defaults(aggregate)
|
|
30
33
|
end
|
data/lib/hecks/version.rb
CHANGED
data/rust/host/HECKS_RELEASE
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
3.4.
|
|
1
|
+
3.4.1
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: hecks
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 3.4.
|
|
4
|
+
version: 3.4.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Chris Young
|
|
@@ -74,6 +74,7 @@ files:
|
|
|
74
74
|
- lib/hecks/adapters/driven/local_storage.rb
|
|
75
75
|
- lib/hecks/adapters/driven/memory.adapter
|
|
76
76
|
- lib/hecks/adapters/driven/memory.rb
|
|
77
|
+
- lib/hecks/adapters/driven/memory/shared_elements.rb
|
|
77
78
|
- lib/hecks/adapters/driven/mock_stripe_adapter.adapter
|
|
78
79
|
- lib/hecks/adapters/driven/mock_stripe_adapter.rb
|
|
79
80
|
- lib/hecks/adapters/driven/postgres.adapter
|