paper_trail_diff 0.14.0 → 0.15.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 +343 -0
- data/QUICKSTART.md +21 -4
- data/README.md +58 -55
- data/docs/architecture.md +32 -11
- data/docs/performance.md +116 -391
- data/docs/reference.md +345 -106
- data/docs/testing.md +63 -7
- data/lib/paper_trail_diff/activity_boundary.rb +83 -26
- data/lib/paper_trail_diff/activity_child_candidate_loader.rb +2 -1
- data/lib/paper_trail_diff/activity_event.rb +41 -10
- data/lib/paper_trail_diff/activity_history.rb +34 -9
- data/lib/paper_trail_diff/activity_range.rb +18 -3
- data/lib/paper_trail_diff/activity_snapshot_sequence.rb +76 -37
- data/lib/paper_trail_diff/activity_target_candidate_loader.rb +5 -5
- data/lib/paper_trail_diff/activity_timeline_builder.rb +266 -111
- data/lib/paper_trail_diff/activity_transaction_grouper.rb +2 -1
- data/lib/paper_trail_diff/activity_tree_filter.rb +114 -0
- data/lib/paper_trail_diff/activity_version_collector.rb +7 -5
- data/lib/paper_trail_diff/activity_view.rb +69 -0
- data/lib/paper_trail_diff/analysis.rb +13 -1
- data/lib/paper_trail_diff/analysis_batch.rb +24 -30
- data/lib/paper_trail_diff/approximate_paths.rb +48 -0
- data/lib/paper_trail_diff/association_activity_edge.rb +6 -6
- data/lib/paper_trail_diff/association_discovery.rb +9 -1
- data/lib/paper_trail_diff/association_key.rb +56 -0
- data/lib/paper_trail_diff/association_traversal.rb +13 -4
- data/lib/paper_trail_diff/batch_boundary_resolver.rb +3 -4
- data/lib/paper_trail_diff/batch_preload.rb +53 -0
- data/lib/paper_trail_diff/batched_root_analyzer.rb +12 -14
- data/lib/paper_trail_diff/batched_root_versions.rb +38 -14
- data/lib/paper_trail_diff/change_row.rb +133 -0
- data/lib/paper_trail_diff/collection_comparator.rb +5 -32
- data/lib/paper_trail_diff/configuration.rb +35 -12
- data/lib/paper_trail_diff/deleted_analysis_batch.rb +18 -18
- data/lib/paper_trail_diff/deleted_root_filter.rb +79 -0
- data/lib/paper_trail_diff/deprecation.rb +61 -0
- data/lib/paper_trail_diff/diagnostics.rb +84 -99
- data/lib/paper_trail_diff/endpoint.rb +26 -3
- data/lib/paper_trail_diff/errors.rb +3 -1
- data/lib/paper_trail_diff/fallback_paths.rb +38 -0
- data/lib/paper_trail_diff/historical_association_reifier.rb +2 -4
- data/lib/paper_trail_diff/historical_filter.rb +19 -10
- data/lib/paper_trail_diff/historical_snapshot_store.rb +57 -67
- data/lib/paper_trail_diff/live_endpoint_batch_loader.rb +1 -35
- data/lib/paper_trail_diff/nested_comparator.rb +19 -18
- data/lib/paper_trail_diff/paper_trail_adapter.rb +122 -127
- data/lib/paper_trail_diff/preloaded_endpoint_batch_loader.rb +13 -1
- data/lib/paper_trail_diff/prepared_activity_index.rb +1 -1
- data/lib/paper_trail_diff/prepared_edge_loader.rb +2 -1
- data/lib/paper_trail_diff/prepared_history.rb +9 -1
- data/lib/paper_trail_diff/prepared_history_loader.rb +1 -0
- data/lib/paper_trail_diff/prepared_record_index.rb +38 -7
- data/lib/paper_trail_diff/root_version_selection.rb +4 -2
- data/lib/paper_trail_diff/scope_condition.rb +76 -21
- data/lib/paper_trail_diff/scope_ranking.rb +12 -2
- data/lib/paper_trail_diff/scoped_analysis.rb +11 -0
- data/lib/paper_trail_diff/scoped_analysis_builder.rb +16 -8
- data/lib/paper_trail_diff/scoped_root_selection.rb +18 -12
- data/lib/paper_trail_diff/snapshot.rb +16 -1
- data/lib/paper_trail_diff/support.rb +29 -2
- data/lib/paper_trail_diff/time_range.rb +32 -10
- data/lib/paper_trail_diff/timeline_builder.rb +7 -7
- data/lib/paper_trail_diff/timeline_range.rb +9 -1
- data/lib/paper_trail_diff/timeline_snapshot_provider.rb +24 -17
- data/lib/paper_trail_diff/transaction_cut.rb +181 -0
- data/lib/paper_trail_diff/traversal_preparer.rb +134 -29
- data/lib/paper_trail_diff/version.rb +1 -1
- data/lib/paper_trail_diff/version_boundary.rb +37 -8
- data/lib/paper_trail_diff/version_query.rb +34 -10
- data/lib/paper_trail_diff/version_scope_filter.rb +9 -10
- data/lib/paper_trail_diff/version_sequence_diagnostics.rb +31 -16
- data/lib/paper_trail_diff/version_table.rb +60 -0
- data/lib/paper_trail_diff.rb +98 -56
- data/sig/generated/paper_trail_diff/activity_boundary.rbs +54 -19
- data/sig/generated/paper_trail_diff/activity_event.rbs +25 -4
- data/sig/generated/paper_trail_diff/activity_history.rbs +21 -6
- data/sig/generated/paper_trail_diff/activity_range.rbs +9 -2
- data/sig/generated/paper_trail_diff/activity_snapshot_sequence.rbs +48 -9
- data/sig/generated/paper_trail_diff/activity_timeline_builder.rbs +134 -30
- data/sig/generated/paper_trail_diff/activity_tree_filter.rbs +73 -0
- data/sig/generated/paper_trail_diff/activity_version_collector.rbs +2 -0
- data/sig/generated/paper_trail_diff/activity_view.rbs +42 -0
- data/sig/generated/paper_trail_diff/analysis.rbs +8 -1
- data/sig/generated/paper_trail_diff/analysis_batch.rbs +8 -15
- data/sig/generated/paper_trail_diff/approximate_paths.rbs +23 -0
- data/sig/generated/paper_trail_diff/association_discovery.rbs +5 -0
- data/sig/generated/paper_trail_diff/association_key.rbs +29 -0
- data/sig/generated/paper_trail_diff/association_traversal.rbs +4 -0
- data/sig/generated/paper_trail_diff/batch_preload.rbs +29 -0
- data/sig/generated/paper_trail_diff/batched_root_analyzer.rbs +9 -10
- data/sig/generated/paper_trail_diff/batched_root_versions.rbs +14 -4
- data/sig/generated/paper_trail_diff/change_row.rbs +86 -0
- data/sig/generated/paper_trail_diff/collection_comparator.rbs +2 -16
- data/sig/generated/paper_trail_diff/configuration.rbs +19 -3
- data/sig/generated/paper_trail_diff/deleted_analysis_batch.rbs +8 -8
- data/sig/generated/paper_trail_diff/deleted_root_filter.rbs +48 -0
- data/sig/generated/paper_trail_diff/deprecation.rbs +35 -0
- data/sig/generated/paper_trail_diff/diagnostics.rbs +37 -30
- data/sig/generated/paper_trail_diff/endpoint.rbs +11 -2
- data/sig/generated/paper_trail_diff/errors.rbs +3 -1
- data/sig/generated/paper_trail_diff/fallback_paths.rbs +22 -0
- data/sig/generated/paper_trail_diff/historical_filter.rbs +12 -2
- data/sig/generated/paper_trail_diff/historical_snapshot_store.rbs +33 -28
- data/sig/generated/paper_trail_diff/live_endpoint_batch_loader.rbs +0 -15
- data/sig/generated/paper_trail_diff/nested_comparator.rbs +10 -7
- data/sig/generated/paper_trail_diff/paper_trail_adapter.rbs +54 -52
- data/sig/generated/paper_trail_diff/preloaded_endpoint_batch_loader.rbs +9 -1
- data/sig/generated/paper_trail_diff/prepared_history.rbs +5 -0
- data/sig/generated/paper_trail_diff/prepared_record_index.rbs +19 -2
- data/sig/generated/paper_trail_diff/root_version_selection.rbs +2 -0
- data/sig/generated/paper_trail_diff/scope_condition.rbs +43 -9
- data/sig/generated/paper_trail_diff/scope_ranking.rbs +8 -0
- data/sig/generated/paper_trail_diff/scoped_analysis.rbs +6 -0
- data/sig/generated/paper_trail_diff/scoped_analysis_builder.rbs +6 -1
- data/sig/generated/paper_trail_diff/scoped_root_selection.rbs +8 -4
- data/sig/generated/paper_trail_diff/snapshot.rbs +8 -0
- data/sig/generated/paper_trail_diff/support.rbs +15 -0
- data/sig/generated/paper_trail_diff/time_range.rbs +20 -6
- data/sig/generated/paper_trail_diff/timeline_builder.rbs +2 -2
- data/sig/generated/paper_trail_diff/timeline_range.rbs +2 -0
- data/sig/generated/paper_trail_diff/timeline_snapshot_provider.rbs +17 -7
- data/sig/generated/paper_trail_diff/transaction_cut.rbs +94 -0
- data/sig/generated/paper_trail_diff/traversal_preparer.rbs +66 -8
- data/sig/generated/paper_trail_diff/version_boundary.rbs +20 -3
- data/sig/generated/paper_trail_diff/version_query.rbs +18 -2
- data/sig/generated/paper_trail_diff/version_scope_filter.rbs +4 -5
- data/sig/generated/paper_trail_diff/version_sequence_diagnostics.rbs +16 -9
- data/sig/generated/paper_trail_diff/version_table.rbs +30 -0
- data/sig/generated/paper_trail_diff.rbs +29 -26
- metadata +46 -10
- data/lib/paper_trail_diff/activity_grouping.rb +0 -38
- data/lib/paper_trail_diff/batch_safety.rb +0 -63
- data/lib/paper_trail_diff/time_activity_timeline_builder.rb +0 -223
- data/sig/generated/paper_trail_diff/activity_grouping.rbs +0 -22
- data/sig/generated/paper_trail_diff/batch_safety.rbs +0 -30
- data/sig/generated/paper_trail_diff/time_activity_timeline_builder.rbs +0 -92
|
@@ -2,24 +2,35 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
|
-
#
|
|
5
|
+
# Prepares the snapshot store for an activity build, one root at a time.
|
|
6
|
+
# Before that, it reads the live record a range closes on.
|
|
6
7
|
class ActivitySnapshotProvider
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
# `snapshotter` reads a full snapshot with a given history, and `refresher`
|
|
9
|
+
# builds the incremental refresher for one root's snapshots.
|
|
10
|
+
#: (store: HistoricalSnapshotStore, snapshotter: untyped, refresher: untyped) -> void
|
|
11
|
+
def initialize(store:, snapshotter:, refresher:)
|
|
12
|
+
@store = store
|
|
10
13
|
@snapshotter = snapshotter
|
|
11
14
|
@refresher = refresher
|
|
12
|
-
@preparer = preparer
|
|
13
15
|
end
|
|
14
16
|
|
|
15
|
-
|
|
17
|
+
# Roots a batch prepared together share its history, so consecutive ones
|
|
18
|
+
# share these snapshots too, and the caches their refresher builds. They
|
|
19
|
+
# are the same for any root with that history, so reusing them depends on
|
|
20
|
+
# nothing but the history.
|
|
21
|
+
#: (untyped, Array[untyped], ?start_at: untyped, ?end_at: untyped) -> PreparedActivitySnapshots
|
|
16
22
|
def prepare(
|
|
17
23
|
record,
|
|
18
24
|
root_versions,
|
|
19
25
|
start_at: root_versions.first.created_at,
|
|
20
26
|
end_at: root_versions.last.created_at
|
|
21
27
|
)
|
|
22
|
-
@
|
|
28
|
+
history = @store.prepare(record, root_versions, start_at: start_at, end_at: end_at)
|
|
29
|
+
last = @last
|
|
30
|
+
return last if last && last.history.equal?(history)
|
|
31
|
+
|
|
32
|
+
@last = PreparedActivitySnapshots.new(history, store: @store, snapshotter: @snapshotter,
|
|
33
|
+
refresher: @refresher)
|
|
23
34
|
end
|
|
24
35
|
|
|
25
36
|
#: (untyped, untyped) -> RecordSnapshot?
|
|
@@ -27,44 +38,73 @@ module PaperTrailDiff
|
|
|
27
38
|
@snapshotter.call(root_endpoint, context_endpoint)
|
|
28
39
|
end
|
|
29
40
|
|
|
41
|
+
# @rbs @store: HistoricalSnapshotStore
|
|
42
|
+
# @rbs @snapshotter: untyped
|
|
43
|
+
# @rbs @refresher: untyped
|
|
44
|
+
# @rbs @last: PreparedActivitySnapshots?
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# One root's full and incremental snapshots during an activity build. Each
|
|
48
|
+
# read passes along the history prepared for this root, so none depends on
|
|
49
|
+
# which root was prepared last.
|
|
50
|
+
class PreparedActivitySnapshots
|
|
51
|
+
attr_reader :history #: PreparedHistory?
|
|
52
|
+
|
|
53
|
+
#: (PreparedHistory?, store: HistoricalSnapshotStore, snapshotter: untyped, refresher: untyped) -> void
|
|
54
|
+
def initialize(history, store:, snapshotter:, refresher:)
|
|
55
|
+
@history = history
|
|
56
|
+
@store = store
|
|
57
|
+
@snapshotter = snapshotter
|
|
58
|
+
@refresher = refresher.call(self)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
#: () -> PreparedActivityIndex?
|
|
62
|
+
def activity_index
|
|
63
|
+
@history&.activity_index
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
#: (untyped, untyped) -> RecordSnapshot?
|
|
67
|
+
def call(root_endpoint, context_endpoint)
|
|
68
|
+
@snapshotter.call(root_endpoint, context_endpoint, history: @history)
|
|
69
|
+
end
|
|
70
|
+
|
|
30
71
|
#: (untyped, untyped, RecordSnapshot?, Array[String], event: ActivityEvent, isolated: bool) -> RecordSnapshot?
|
|
31
|
-
def refresh( # rubocop:disable Metrics/ParameterLists
|
|
32
|
-
root_endpoint,
|
|
33
|
-
|
|
34
|
-
previous_snapshot,
|
|
35
|
-
branches,
|
|
36
|
-
event:,
|
|
37
|
-
isolated:
|
|
38
|
-
)
|
|
39
|
-
@refresher.call(
|
|
40
|
-
root_endpoint,
|
|
41
|
-
context_endpoint,
|
|
42
|
-
previous_snapshot,
|
|
43
|
-
branches,
|
|
44
|
-
event: event,
|
|
45
|
-
isolated: isolated
|
|
46
|
-
)
|
|
72
|
+
def refresh(root_endpoint, context_endpoint, previous_snapshot, branches, event:, isolated:) # rubocop:disable Metrics/ParameterLists
|
|
73
|
+
@refresher.call(root_endpoint, context_endpoint, previous_snapshot, branches,
|
|
74
|
+
event: event, isolated: isolated)
|
|
47
75
|
end
|
|
48
76
|
|
|
49
77
|
#: (untyped, untyped, RecordSnapshot?, event: ActivityEvent) -> RecordSnapshot?
|
|
50
78
|
def advance_root(root_endpoint, context_endpoint, previous_snapshot, event:)
|
|
51
|
-
@refresher.advance_root(
|
|
52
|
-
root_endpoint,
|
|
53
|
-
context_endpoint,
|
|
54
|
-
previous_snapshot,
|
|
55
|
-
event: event
|
|
56
|
-
)
|
|
79
|
+
@refresher.advance_root(root_endpoint, context_endpoint, previous_snapshot, event: event)
|
|
57
80
|
end
|
|
58
81
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
82
|
+
# The branches a refresh rebuilds, with this root's history.
|
|
83
|
+
#: (untyped, untyped, tree: AssociationTree, normalizer: SnapshotNormalizer) -> RecordSnapshot?
|
|
84
|
+
def custom(root_endpoint, context_endpoint, tree:, normalizer:)
|
|
85
|
+
@store.custom(root_endpoint, context_endpoint, tree: tree, normalizer: normalizer,
|
|
86
|
+
history: @history)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
#: (untyped, habtm_version: untyped) -> untyped
|
|
90
|
+
def association_reader(context_version, habtm_version:)
|
|
91
|
+
@store.association_reader(context_version, habtm_version: habtm_version, history: @history)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
#: (untyped) -> [Hash[untyped, untyped], Hash[untyped, untyped]]?
|
|
95
|
+
def record_transition(version)
|
|
96
|
+
@history&.record_transition(Endpoint.model_class(version), version.item_id, version)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
#: (untyped, ?direct: bool) -> [bool, untyped]
|
|
100
|
+
def record_after(version, direct: false)
|
|
101
|
+
@history&.record_after(Endpoint.model_class(version), version.item_id, version,
|
|
102
|
+
direct: direct) || [false, nil]
|
|
62
103
|
end
|
|
63
104
|
|
|
64
|
-
# @rbs @
|
|
105
|
+
# @rbs @store: HistoricalSnapshotStore
|
|
65
106
|
# @rbs @snapshotter: untyped
|
|
66
107
|
# @rbs @refresher: untyped
|
|
67
|
-
# @rbs @preparer: untyped
|
|
68
108
|
end
|
|
69
109
|
|
|
70
110
|
# Builds chronological snapshots, refreshing only branches affected by prior events.
|
|
@@ -116,7 +156,7 @@ module PaperTrailDiff
|
|
|
116
156
|
incremental_root?(previous_event)
|
|
117
157
|
|
|
118
158
|
branches = @transaction_groups.branches_for(previous_event)
|
|
119
|
-
return full_snapshot(event) unless branches
|
|
159
|
+
return full_snapshot(event) unless branches
|
|
120
160
|
|
|
121
161
|
@snapshotter.refresh(
|
|
122
162
|
root_endpoint(event.version),
|
|
@@ -130,8 +170,7 @@ module PaperTrailDiff
|
|
|
130
170
|
|
|
131
171
|
#: (ActivityEvent) -> bool
|
|
132
172
|
def incremental_root?(event)
|
|
133
|
-
event.root? && @transaction_groups.isolated?(event)
|
|
134
|
-
@snapshotter.respond_to?(:advance_root)
|
|
173
|
+
event.root? && @transaction_groups.isolated?(event)
|
|
135
174
|
end
|
|
136
175
|
|
|
137
176
|
#: (ActivityEvent, ActivityEvent, RecordSnapshot?) -> RecordSnapshot?
|
|
@@ -83,11 +83,11 @@ module PaperTrailDiff
|
|
|
83
83
|
|
|
84
84
|
#: (untyped) -> untyped
|
|
85
85
|
def historical_relation(version_class)
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
86
|
+
VersionTable.joined(association_class, version_class)
|
|
87
|
+
.where(foreign_key_name: @reflection.foreign_key)
|
|
88
|
+
.where(version_class.table_name => {
|
|
89
|
+
item_type: @owner_class.base_class.name, item_id: @owner_ids
|
|
90
|
+
})
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
#: (untyped, untyped) -> Array[Array[untyped]]
|
|
@@ -2,18 +2,17 @@
|
|
|
2
2
|
# rbs_inline: enabled
|
|
3
3
|
|
|
4
4
|
module PaperTrailDiff
|
|
5
|
-
# Compares adjacent root and selected-descendant activity boundaries
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
@retain = snapshots || grouping?
|
|
5
|
+
# Compares adjacent root and selected-descendant activity boundaries, between
|
|
6
|
+
# explicit endpoints or over a wall-clock window. Both replay the range's
|
|
7
|
+
# events into steps the same way, and close on the live row the same way.
|
|
8
|
+
# They differ in what they report and what closes them: explicit endpoints
|
|
9
|
+
# report every step between them and close on the endpoint asked for; a
|
|
10
|
+
# window reports only the events inside it, and closes on a final destroy,
|
|
11
|
+
# or on the live row when nothing recorded follows it.
|
|
12
|
+
class ActivityTimelineBuilder # rubocop:disable Metrics/ClassLength
|
|
13
|
+
#: (untyped, range: TimelineRange, tree: AssociationTree, snapshotter: untyped, view: ActivityView) -> void
|
|
14
|
+
def initialize(record, range:, tree:, snapshotter:, view:)
|
|
15
|
+
@view = view
|
|
17
16
|
@record = record
|
|
18
17
|
@to = range.to
|
|
19
18
|
@range = range
|
|
@@ -23,7 +22,7 @@ module PaperTrailDiff
|
|
|
23
22
|
|
|
24
23
|
#: () -> Array[ActivityStep]
|
|
25
24
|
def build
|
|
26
|
-
return
|
|
25
|
+
return window_steps if @range.time?
|
|
27
26
|
return no_steps if @range.unresolved?
|
|
28
27
|
return build_to_current if Endpoint.record?(@to)
|
|
29
28
|
|
|
@@ -31,20 +30,18 @@ module PaperTrailDiff
|
|
|
31
30
|
build_between_versions
|
|
32
31
|
end
|
|
33
32
|
|
|
34
|
-
# Builds
|
|
33
|
+
# Builds the endpoint diff, the root timeline, and the activity from one
|
|
34
|
+
# activity snapshot pass.
|
|
35
35
|
#: () -> Analysis
|
|
36
36
|
def analyze
|
|
37
|
-
return
|
|
37
|
+
return analyze_window if @range.time?
|
|
38
38
|
return Analysis.empty if @range.unresolved?
|
|
39
39
|
return analyze_current if Endpoint.record?(@to)
|
|
40
40
|
|
|
41
41
|
plan = @range.select_plan
|
|
42
42
|
return Analysis.empty if plan.empty?
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
prepare_history(root_versions)
|
|
46
|
-
events = collect_events(root_versions)
|
|
47
|
-
build_analysis(plan, events, event_history(root_versions, events))
|
|
44
|
+
build_analysis(plan, version_history(plan.reconstruction_versions))
|
|
48
45
|
end
|
|
49
46
|
|
|
50
47
|
private
|
|
@@ -54,7 +51,7 @@ module PaperTrailDiff
|
|
|
54
51
|
# @rbs @range: TimelineRange
|
|
55
52
|
# @rbs @tree: AssociationTree
|
|
56
53
|
# @rbs @snapshotter: untyped
|
|
57
|
-
# @rbs @
|
|
54
|
+
# @rbs @view: ActivityView
|
|
58
55
|
|
|
59
56
|
#: () -> Array[ActivityStep]
|
|
60
57
|
def no_steps
|
|
@@ -73,56 +70,66 @@ module PaperTrailDiff
|
|
|
73
70
|
return no_steps if plan.empty?
|
|
74
71
|
|
|
75
72
|
root_versions = plan.reconstruction_versions
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
73
|
+
activity_steps(version_history(root_versions), destroyed_boundary(root_versions), nil)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Activity from the first to the last of the root versions.
|
|
77
|
+
#: (Array[untyped]) -> ActivityHistory
|
|
78
|
+
def version_history(root_versions)
|
|
79
|
+
snapshots = prepare_history(root_versions)
|
|
80
|
+
events = collect_events(root_versions, snapshots, range_end: root_versions.last)
|
|
81
|
+
replay(root_versions, events, snapshots)
|
|
80
82
|
end
|
|
81
83
|
|
|
82
84
|
#: () -> Array[ActivityStep]
|
|
83
85
|
def build_to_current
|
|
84
|
-
_plan,
|
|
85
|
-
activity_steps(history,
|
|
86
|
+
_plan, history, snapshot, boundary = current_history
|
|
87
|
+
activity_steps(history, boundary, snapshot)
|
|
86
88
|
end
|
|
87
89
|
|
|
88
90
|
#: () -> Analysis
|
|
89
91
|
def analyze_current
|
|
90
|
-
plan,
|
|
91
|
-
final
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
timeline: ActivityRootSteps.call(
|
|
95
|
-
plan, history.root_snapshots, closing_snapshot: snapshot,
|
|
96
|
-
captured_at: boundary&.recorded_at
|
|
97
|
-
),
|
|
98
|
-
activity_timeline: activity_steps(history, events, boundary, snapshot),
|
|
99
|
-
from_snapshot: history.first_snapshot, to_snapshot: final
|
|
100
|
-
)
|
|
92
|
+
plan, history, snapshot, boundary = current_history
|
|
93
|
+
analysis(plan, history, final: boundary ? snapshot : history.last_snapshot,
|
|
94
|
+
steps: activity_steps(history, boundary, snapshot),
|
|
95
|
+
closing_snapshot: snapshot, captured_at: boundary&.recorded_at)
|
|
101
96
|
end
|
|
102
97
|
|
|
103
98
|
# The explicit live endpoint and a batch's live closing endpoint use the
|
|
104
99
|
# same selected plan. A version filter can close earlier, on its successor.
|
|
105
|
-
#: () -> [RootVersionPlan,
|
|
100
|
+
#: () -> [RootVersionPlan, ActivityHistory, RecordSnapshot?, ActivityBoundary?]
|
|
106
101
|
def current_history
|
|
107
102
|
validate_current_range!
|
|
108
103
|
plan = @range.select_plan
|
|
109
104
|
return empty_history(plan) if plan.empty?
|
|
110
105
|
|
|
111
106
|
snapshot, captured_at = capture_current if plan.closing_record
|
|
107
|
+
history = history_until(plan, snapshot, captured_at)
|
|
108
|
+
boundary = current_boundary(captured_at) if plan.closing_record
|
|
109
|
+
[plan, history, snapshot, boundary]
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Events up to the instant the current record was read. A range that a
|
|
113
|
+
# version filter closes earlier ends at that version, with no snapshot.
|
|
114
|
+
#: (RootVersionPlan, RecordSnapshot?, untyped) -> ActivityHistory
|
|
115
|
+
def history_until(plan, snapshot, captured_at)
|
|
112
116
|
versions = plan.reconstruction_versions
|
|
113
|
-
prepare_history(versions, end_at: captured_at)
|
|
114
|
-
events = collect_events(versions, range_end: captured_at || versions.last)
|
|
115
|
-
|
|
116
|
-
boundary = ActivityBoundary.current(@to, captured_at: captured_at) if plan.closing_record
|
|
117
|
-
[plan, events, history, snapshot, boundary]
|
|
117
|
+
snapshots = prepare_history(versions, end_at: captured_at)
|
|
118
|
+
events = collect_events(versions, snapshots, range_end: captured_at || versions.last)
|
|
119
|
+
replay(versions, events, snapshots, current: plan.closing_record, closing: snapshot)
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
# Without a closing boundary nothing reads the
|
|
121
|
-
#
|
|
122
|
+
# Without a closing boundary nothing reads the closing pair, so its
|
|
123
|
+
# mutations cannot change an answer.
|
|
122
124
|
# mutant:disable
|
|
123
|
-
#: (RootVersionPlan) -> [RootVersionPlan,
|
|
125
|
+
#: (RootVersionPlan) -> [RootVersionPlan, ActivityHistory, RecordSnapshot?, ActivityBoundary?]
|
|
124
126
|
def empty_history(plan)
|
|
125
|
-
[plan,
|
|
127
|
+
[plan, ActivityHistory.empty, nil, nil]
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
#: (untyped) -> ActivityBoundary
|
|
131
|
+
def current_boundary(captured_at)
|
|
132
|
+
ActivityBoundary.current(@to, captured_at: captured_at, recorded: root_recorded)
|
|
126
133
|
end
|
|
127
134
|
|
|
128
135
|
#: () -> [RecordSnapshot?, untyped]
|
|
@@ -137,49 +144,20 @@ module PaperTrailDiff
|
|
|
137
144
|
end
|
|
138
145
|
|
|
139
146
|
Endpoint.validate_pair!(@range.from, @to)
|
|
140
|
-
|
|
141
|
-
end
|
|
142
|
-
|
|
143
|
-
#: (Array[untyped], ?range_start: untyped, ?range_end: untyped) -> Array[ActivityEvent]
|
|
144
|
-
def collect_events(
|
|
145
|
-
root_versions,
|
|
146
|
-
range_start: root_versions.first,
|
|
147
|
-
range_end: root_versions.last
|
|
148
|
-
)
|
|
149
|
-
ActivityVersionCollector.new(
|
|
150
|
-
@record,
|
|
151
|
-
root_versions: root_versions,
|
|
152
|
-
tree: @tree,
|
|
153
|
-
prepared: (@snapshotter.activity_index if @snapshotter.respond_to?(:activity_index)),
|
|
154
|
-
range_start: range_start,
|
|
155
|
-
range_end: range_end
|
|
156
|
-
).call
|
|
157
|
-
end
|
|
158
|
-
|
|
159
|
-
#: (Array[untyped], ?end_at: untyped) -> void
|
|
160
|
-
def prepare_history(root_versions, end_at: nil)
|
|
161
|
-
return unless @snapshotter.respond_to?(:prepare)
|
|
162
|
-
return @snapshotter.prepare(@record, root_versions) unless end_at
|
|
163
|
-
|
|
164
|
-
@snapshotter.prepare(@record, root_versions, end_at: end_at)
|
|
147
|
+
reject_live_habtm!
|
|
165
148
|
end
|
|
166
149
|
|
|
167
150
|
# Appends the transition into an explicit closing boundary, which is either
|
|
168
151
|
# a requested current record or the absence a final root destroy leaves.
|
|
169
|
-
# Either follows the root versions, which are themselves events
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
from_snapshot: history.last_snapshot, to_snapshot: final_snapshot,
|
|
179
|
-
retain: @retain
|
|
180
|
-
)
|
|
181
|
-
end
|
|
182
|
-
group_steps(steps).freeze
|
|
152
|
+
# Either follows the root versions, which are themselves events, so the
|
|
153
|
+
# last boundary the history kept opens it.
|
|
154
|
+
#: (ActivityHistory, ActivityBoundary?, RecordSnapshot?) -> Array[ActivityStep]
|
|
155
|
+
def activity_steps(history, final_boundary, final_snapshot)
|
|
156
|
+
return present(history.steps) unless final_boundary
|
|
157
|
+
|
|
158
|
+
last_boundary = history.last_boundary #: ActivityBoundary
|
|
159
|
+
present(history.steps + [step_from_last(history, last_boundary, final_boundary,
|
|
160
|
+
final_snapshot)])
|
|
183
161
|
end
|
|
184
162
|
|
|
185
163
|
# A destroyed root has no later version, but its own event states that
|
|
@@ -189,41 +167,218 @@ module PaperTrailDiff
|
|
|
189
167
|
version = root_versions.last
|
|
190
168
|
return unless version.event == 'destroy'
|
|
191
169
|
|
|
192
|
-
ActivityBoundary.destroyed(version)
|
|
170
|
+
ActivityBoundary.destroyed(version, recorded: root_recorded)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
# Only the activity view gains the closing removal. The endpoint diff and
|
|
174
|
+
# the root timeline keep their `compare` and `timeline` semantics, under
|
|
175
|
+
# which the state at a destroy version is the state before the deletion.
|
|
176
|
+
#: (RootVersionPlan, ActivityHistory) -> Analysis
|
|
177
|
+
def build_analysis(plan, history)
|
|
178
|
+
steps = activity_steps(history, destroyed_boundary(plan.reconstruction_versions), nil)
|
|
179
|
+
analysis(plan, history, final: history.last_snapshot, steps: steps)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
#: () -> Array[ActivityStep]
|
|
183
|
+
def window_steps
|
|
184
|
+
history, _plan, closing, = window_history
|
|
185
|
+
present(closing ? history.steps + [closing] : history.steps)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
#: () -> Analysis
|
|
189
|
+
def analyze_window
|
|
190
|
+
history, plan, closing, closing_snapshot, captured_at = window_history
|
|
191
|
+
analysis(plan, history,
|
|
192
|
+
final: window_final_snapshot(history, closing, closing_snapshot),
|
|
193
|
+
steps: present(closing ? history.steps + [closing] : history.steps),
|
|
194
|
+
closing_snapshot: closing_snapshot, captured_at: captured_at)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# A window with a closing step ends at what closed it: the live row, or
|
|
198
|
+
# nothing after a destroy, where no live row is read. One that ends before
|
|
199
|
+
# a later event ends at the state that event found.
|
|
200
|
+
#: (ActivityHistory, ActivityStep?, RecordSnapshot?) -> RecordSnapshot?
|
|
201
|
+
def window_final_snapshot(history, closing, closing_snapshot)
|
|
202
|
+
closing ? closing_snapshot : history.last_snapshot
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
#: () -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
|
|
206
|
+
def window_history
|
|
207
|
+
plan = @range.select_plan(context_required: !@tree.empty?)
|
|
208
|
+
reject_live_habtm! if plan.closing_record
|
|
209
|
+
return empty_window(plan) if plan.empty? && !plan.closing_record
|
|
210
|
+
|
|
211
|
+
# A window closing on current state runs to the instant it is captured,
|
|
212
|
+
# not to the last root version, or descendants that moved after that
|
|
213
|
+
# version would be missing from a report that claims to reach now.
|
|
214
|
+
window_replay(plan, (Time.now.utc if plan.closing_record))
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
# Nothing reads a window's closing step, live snapshot, or capture time
|
|
218
|
+
# when it reported nothing, so mutations of those cannot change an answer.
|
|
219
|
+
# mutant:disable
|
|
220
|
+
#: (RootVersionPlan) -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
|
|
221
|
+
def empty_window(plan)
|
|
222
|
+
[ActivityHistory.empty, plan, nil, nil, nil]
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
# A window with no selected event reports nothing. Any other has a
|
|
226
|
+
# boundary after its last one: the plan refuses a window that has no later
|
|
227
|
+
# version, ends on no destroy, and does not close on the live row.
|
|
228
|
+
#: (RootVersionPlan, untyped) -> [ActivityHistory, RootVersionPlan, ActivityStep?, RecordSnapshot?, untyped]
|
|
229
|
+
def window_replay(plan, captured_at)
|
|
230
|
+
versions = plan.reconstruction_versions
|
|
231
|
+
snapshots = prepare_history(versions, end_at: captured_at)
|
|
232
|
+
events = collect_events(versions, snapshots, range_end: captured_at || versions.last)
|
|
233
|
+
selected = selected_events(events, plan)
|
|
234
|
+
last = selected.last
|
|
235
|
+
return empty_window(plan) unless last
|
|
236
|
+
|
|
237
|
+
snapshot = window_live_snapshot(plan)
|
|
238
|
+
history = replay(versions, events, snapshots, current: plan.closing_record, closing: snapshot)
|
|
239
|
+
closing = window_closing(history, last, plan, captured_at, snapshot, events)
|
|
240
|
+
[history, plan, closing, snapshot, captured_at]
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
# One live read serves both the closing activity step and the endpoint the
|
|
244
|
+
# analysis reports, which would otherwise reconstruct the graph twice.
|
|
245
|
+
#: (RootVersionPlan) -> RecordSnapshot?
|
|
246
|
+
def window_live_snapshot(plan)
|
|
247
|
+
record = plan.closing_record
|
|
248
|
+
return unless record
|
|
249
|
+
|
|
250
|
+
@snapshotter.call(record, record)
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
# Root versions inside the window that the plan does not report are context
|
|
254
|
+
# rather than mutations: the version appended to reveal the last selected
|
|
255
|
+
# change, and anything a filter excluded but the replay still walks. Neither
|
|
256
|
+
# may be counted as a selected mutation. Descendant events are not filtered,
|
|
257
|
+
# so window membership is the whole test for them.
|
|
258
|
+
#: (Array[ActivityEvent], RootVersionPlan) -> Array[ActivityEvent]
|
|
259
|
+
def selected_events(events, plan)
|
|
260
|
+
events.select do |event|
|
|
261
|
+
@range.include?(event.version) && (!event.root? || plan.mutation?(event.version))
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
# A window either closes on the absence its final destruction leaves, or on
|
|
266
|
+
# current state when it reaches past everything recorded. Both are boundaries
|
|
267
|
+
# no version can supply.
|
|
268
|
+
#
|
|
269
|
+
# Everything recorded means on every requested path, not only the root's
|
|
270
|
+
# own versions. When an association changed after the window, its last
|
|
271
|
+
# step already ends at that change, at the state the window left; closing
|
|
272
|
+
# on current state would add the later change to a window it was not in.
|
|
273
|
+
#: (ActivityHistory, ActivityEvent, RootVersionPlan, untyped, RecordSnapshot?, Array[ActivityEvent]) -> ActivityStep?
|
|
274
|
+
def window_closing(history, event, plan, captured_at, snapshot, events) # rubocop:disable Metrics/ParameterLists
|
|
275
|
+
return window_destroyed_step(history, event) if terminal_destroy?(event)
|
|
276
|
+
return unless plan.closing_record
|
|
277
|
+
return if events.any? { |later| !@range.include?(later.version) }
|
|
278
|
+
|
|
279
|
+
window_current_step(history, plan.closing_record, captured_at, snapshot)
|
|
193
280
|
end
|
|
194
281
|
|
|
195
|
-
#: (
|
|
196
|
-
def
|
|
282
|
+
#: (ActivityHistory, ActivityEvent) -> ActivityStep
|
|
283
|
+
def window_destroyed_step(history, event)
|
|
284
|
+
version = event.version
|
|
285
|
+
ActivityStep.between(
|
|
286
|
+
from_boundary: ActivityBoundary.from_version(version, recorded: root_recorded),
|
|
287
|
+
to_boundary: ActivityBoundary.destroyed(version, recorded: root_recorded),
|
|
288
|
+
from_snapshot: history.root_snapshots.fetch(ActivityRootSteps.version_key(version)),
|
|
289
|
+
to_snapshot: nil, retain: @view.retain?
|
|
290
|
+
)
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
# The last boundary reached is where current state is compared from, which
|
|
294
|
+
# is the final event rather than the final root version once descendants
|
|
295
|
+
# have moved since it. A selected event is a boundary the history kept.
|
|
296
|
+
#: (ActivityHistory, untyped, untyped, RecordSnapshot?) -> ActivityStep
|
|
297
|
+
def window_current_step(history, record, captured_at, snapshot)
|
|
298
|
+
previous = history.last_boundary #: ActivityBoundary
|
|
299
|
+
current = ActivityBoundary.current(record, captured_at: captured_at, recorded: root_recorded)
|
|
300
|
+
step_from_last(history, previous, current, snapshot)
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
#: (ActivityEvent) -> bool
|
|
304
|
+
def terminal_destroy?(event)
|
|
305
|
+
event.root? && event.version.event == 'destroy'
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
#: () -> void
|
|
309
|
+
def reject_live_habtm!
|
|
310
|
+
AssociationTraversal.new(@tree).reject_live_habtm_activity!(@record.class)
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
#: () -> Array[String]?
|
|
314
|
+
def root_recorded
|
|
315
|
+
@view.recorded_attributes(root: true)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
# A window prepares from its start, explicit endpoints from their first
|
|
319
|
+
# version.
|
|
320
|
+
#: (Array[untyped], ?end_at: untyped) -> untyped
|
|
321
|
+
def prepare_history(root_versions, end_at: nil)
|
|
322
|
+
options = { start_at: @range.begin_time, end_at: end_at }.compact
|
|
323
|
+
@snapshotter.prepare(@record, root_versions, **options)
|
|
324
|
+
end
|
|
325
|
+
|
|
326
|
+
# A window collects from its start, explicit endpoints from their first
|
|
327
|
+
# version. Explicit endpoints also stop before the rest of the closing
|
|
328
|
+
# version's transaction (see ActivityTransactionGroups.before_closing); a
|
|
329
|
+
# window keeps every event it collected and reports only those inside it.
|
|
330
|
+
#: (Array[untyped], untyped, range_end: untyped) -> Array[ActivityEvent]
|
|
331
|
+
def collect_events(root_versions, snapshots, range_end:)
|
|
332
|
+
events = ActivityVersionCollector.new(
|
|
333
|
+
@record,
|
|
334
|
+
root_versions: root_versions,
|
|
335
|
+
tree: @tree,
|
|
336
|
+
prepared: snapshots.activity_index,
|
|
337
|
+
range_start: @range.begin_time || root_versions.first,
|
|
338
|
+
range_end: range_end
|
|
339
|
+
).call
|
|
340
|
+
@range.time? ? events : ActivityTransactionGroups.before_closing(events, range_end)
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Events after the last root version read the root from the live record
|
|
344
|
+
# when the range closes on it. Explicit endpoints report every step, as
|
|
345
|
+
# their range includes every version.
|
|
346
|
+
#: (Array[untyped], Array[ActivityEvent], untyped, ?current: untyped, ?closing: RecordSnapshot?) -> ActivityHistory
|
|
347
|
+
def replay(root_versions, events, snapshots, current: nil, closing: nil)
|
|
197
348
|
ActivityHistoryBuilder.new(
|
|
198
|
-
root_versions,
|
|
199
|
-
events,
|
|
200
|
-
@snapshotter,
|
|
349
|
+
root_versions, events, snapshots,
|
|
201
350
|
current: current,
|
|
202
|
-
|
|
351
|
+
include_step: ->(event) { @range.include?(event.version) },
|
|
352
|
+
view: @view,
|
|
353
|
+
closing: closing
|
|
203
354
|
).call
|
|
204
355
|
end
|
|
205
356
|
|
|
206
|
-
#
|
|
207
|
-
# the
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
activity_timeline: activity_steps(
|
|
215
|
-
history, events, destroyed_boundary(plan.reconstruction_versions), nil
|
|
216
|
-
),
|
|
217
|
-
from_snapshot: history.first_snapshot,
|
|
218
|
-
to_snapshot: history.last_snapshot
|
|
357
|
+
# The step from the last boundary the history reached to one that closes
|
|
358
|
+
# the range.
|
|
359
|
+
#: (ActivityHistory, ActivityBoundary, ActivityBoundary, RecordSnapshot?) -> ActivityStep
|
|
360
|
+
def step_from_last(history, last_boundary, closing_boundary, closing_snapshot)
|
|
361
|
+
ActivityStep.between(
|
|
362
|
+
from_boundary: last_boundary, to_boundary: closing_boundary,
|
|
363
|
+
from_snapshot: history.last_event_snapshot, to_snapshot: closing_snapshot,
|
|
364
|
+
retain: @view.retain?
|
|
219
365
|
)
|
|
220
366
|
end
|
|
221
367
|
|
|
222
|
-
#: () ->
|
|
223
|
-
def
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
368
|
+
#: (Array[ActivityStep]) -> Array[ActivityStep]
|
|
369
|
+
def present(steps)
|
|
370
|
+
@view.present(steps).freeze
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
#: (RootVersionPlan, ActivityHistory, final: RecordSnapshot?, steps: Array[ActivityStep], ?closing_snapshot: RecordSnapshot?, ?captured_at: untyped) -> Analysis
|
|
374
|
+
def analysis(plan, history, final:, steps:, **closing)
|
|
375
|
+
Analysis.new(
|
|
376
|
+
diff: Engine.compare(history.first_snapshot, final),
|
|
377
|
+
timeline: ActivityRootSteps.call(plan, history.root_snapshots, **closing),
|
|
378
|
+
activity_timeline: steps,
|
|
379
|
+
from_snapshot: history.first_snapshot,
|
|
380
|
+
to_snapshot: final
|
|
381
|
+
)
|
|
227
382
|
end
|
|
228
383
|
end
|
|
229
384
|
end
|