paper_trail_diff 0.7.0 → 0.7.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 33b445f893d6d95faf0ad22b72d35e8b248f3c00240e537898cac2d067ece043
4
- data.tar.gz: 57ee0b8fc242825852726933a9aa332cd309c38d2af21724a41c4bfcfbf2bd41
3
+ metadata.gz: a84116670d1dd65a6cf569a266ccc8587a9eab4e65d38cf84df485c26d33d9dc
4
+ data.tar.gz: 2c8ddb0fd1b2203e644f3c50ed24da33ed25cb4c4240665eab85368cd4534f22
5
5
  SHA512:
6
- metadata.gz: bbbdea8be4b4300705da0f915dbe3b61ede862817fa27c44f0371c8454f9995f20b16120acc43cf0272f8fdecfdef4fa5fce160642812805a2c636f6f6acf9a0
7
- data.tar.gz: e6f9a063c376a1214b1deacbd0ea15e325e82a1cccecd86d3d747268471bac0b20585249a4dcb1f039730f39bb019cc35a7e12a2e0bbfb963d4cc856acd6d2ef
6
+ metadata.gz: 3e32ecbedd636456388129cc664cf688b0aa909d5c05b45124eb1a6bd82dabbffa96557fe5efc946e93fe459234d3c4c5ce9af11f39228ddab55228f5a53e2f9
7
+ data.tar.gz: 1b29680ab942334fd98ce5cc0a4f2bc823fd180b258282e8be2c07ba784c9c3b5aa9edc76e0e118fec3baabcfbc94344c208aef116702874c0eb4dd226cddcac
data/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
3
3
  All notable changes to this project will be documented in this file. The
4
4
  project follows [Semantic Versioning](https://semver.org/).
5
5
 
6
+ ## [0.7.1] - 2026-08-11
7
+
8
+ ### Fixed
9
+
10
+ - Load each current endpoint once per call instead of once per request for it,
11
+ so `close_on: :current` no longer costs a live read per root and
12
+ `analyze_many` keeps the flat query cost it exists for. A twenty-root batch
13
+ closing on current state issued 43 queries against the 3 a closed window
14
+ needs. Every root in one result now also reflects the same instant rather
15
+ than whenever its turn came.
16
+
17
+ ### Changed
18
+
19
+ - Name the way out in `IncompleteTimeRangeError`: the message now points at
20
+ `close_on: :current` and at narrowing the window. Its previous advice — add a
21
+ checkpoint after the window — cannot be followed by a report about the
22
+ present, which is the case that raises it most.
23
+ - Correct the `reload_live_endpoints:` documentation, which claimed `timeline`
24
+ and `analyze` never read live state. They do under `close_on: :current`, and
25
+ they do not accept the option.
26
+
6
27
  ## [0.7.0] - 2026-08-11
7
28
 
8
29
  ### Added
data/README.md CHANGED
@@ -242,8 +242,13 @@ must observe one atomic snapshot.
242
242
 
243
243
  `compare`, `compare_many`, and `activity_timeline(..., to: record)` reload
244
244
  current endpoints by default. A caller that already owns a consistent, fully
245
- preloaded graph may opt out. The option has no effect on `timeline` or
246
- `analyze`, which are bounded by versions and never read live state:
245
+ preloaded graph may opt out. Each endpoint is loaded once per call, so a batch
246
+ that resolves every root up front and then reads one of them again pays
247
+ nothing the second time, and every root in one result reflects the same instant.
248
+
249
+ The option is not accepted by `timeline`, `analyze`, or `analyze_many`. Those
250
+ are bounded by versions and read live state only under
251
+ [`close_on: :current`](#reporting-up-to-now), which always reloads:
247
252
 
248
253
  ```ruby
249
254
  orders = Order.where(id: order_ids).preload(line_items: :product).to_a
@@ -23,18 +23,37 @@ module PaperTrailDiff
23
23
  else
24
24
  PreloadedEndpointBatchLoader.new(tree: tree, traversal: traversal)
25
25
  end
26
+ @loaded = {} #: Hash[identity, untyped]
26
27
  end
27
28
 
29
+ # Loads each endpoint at most once for the life of this provider, which is
30
+ # one public call. A batch that resolves every root up front and then asks
31
+ # again for one of them costs nothing the second time, and every root in the
32
+ # result reflects the same instant rather than whenever its turn came.
28
33
  #: (Array[untyped]) -> Hash[identity, untyped]
29
34
  def call(records)
30
- Instrumentation.instrument('load_live_endpoints', payload(records)) do
31
- @loader.call(records)
35
+ missing = records.reject { |record| @loaded.key?(Endpoint.identity(record)) }
36
+ return known(records) if missing.empty?
37
+
38
+ loaded = Instrumentation.instrument('load_live_endpoints', payload(missing)) do
39
+ @loader.call(missing)
32
40
  end
41
+ @loaded.merge!(loaded)
42
+ known(records)
33
43
  end
34
44
 
35
45
  private
36
46
 
37
47
  # @rbs @loader: untyped
48
+ # @rbs @loaded: Hash[identity, untyped]
49
+
50
+ #: (Array[untyped]) -> Hash[identity, untyped]
51
+ def known(records)
52
+ records.to_h do |record|
53
+ identity = Endpoint.identity(record)
54
+ [identity, @loaded.fetch(identity)]
55
+ end
56
+ end
38
57
 
39
58
  #: (Array[untyped]) -> Hash[Symbol, untyped]
40
59
  def payload(records)
@@ -11,7 +11,12 @@ module PaperTrailDiff
11
11
  # Both the single-record and the batched selectors resolve this here, because
12
12
  # the rule is subtle enough that two copies of it would drift apart.
13
13
  class RootVersionSelection
14
- INCOMPLETE = 'time range requires a later root version to reconstruct its final change'
14
+ # An error is a specification, so it names the way out as well as the wall.
15
+ # A window ending at the present can never gain a later version, which makes
16
+ # "write a checkpoint afterwards" impossible advice on its own.
17
+ INCOMPLETE = 'time range requires a later root version to reconstruct its ' \
18
+ 'final change: pass close_on: :current to end at current state, ' \
19
+ 'or narrow the window to end before the last recorded version'
15
20
 
16
21
  #: (in_range: Array[untyped], selected: Array[untyped], after_range: untyped, windowed: bool, ?context_required: bool, ?filtered: bool, ?live_endpoint: untyped) -> void
17
22
  def initialize( # rubocop:disable Metrics/ParameterLists
@@ -187,7 +187,9 @@ module PaperTrailDiff
187
187
  return true if terminal_destroy?(selected.last)
188
188
  return true if plan.closing_record
189
189
 
190
- message = 'time range requires a later activity boundary to reconstruct its final change'
190
+ message = 'time range requires a later activity boundary to reconstruct its ' \
191
+ 'final change: pass close_on: :current to end at current state, ' \
192
+ 'or narrow the window to end before the last recorded boundary'
191
193
  raise IncompleteTimeRangeError, message
192
194
  end
193
195
 
@@ -2,5 +2,5 @@
2
2
  # rbs_inline: enabled
3
3
 
4
4
  module PaperTrailDiff
5
- VERSION = '0.7.0'
5
+ VERSION = '0.7.1'
6
6
  end
@@ -10,13 +10,22 @@ module PaperTrailDiff
10
10
  # : (tree: AssociationTree, traversal: AssociationTraversal, reload: bool) -> void
11
11
  def initialize: (tree: AssociationTree, traversal: AssociationTraversal, reload: bool) -> void
12
12
 
13
+ # Loads each endpoint at most once for the life of this provider, which is
14
+ # one public call. A batch that resolves every root up front and then asks
15
+ # again for one of them costs nothing the second time, and every root in the
16
+ # result reflects the same instant rather than whenever its turn came.
13
17
  # : (Array[untyped]) -> Hash[identity, untyped]
14
18
  def call: (Array[untyped]) -> Hash[identity, untyped]
15
19
 
16
20
  private
17
21
 
22
+ @loaded: Hash[identity, untyped]
23
+
18
24
  @loader: untyped
19
25
 
26
+ # : (Array[untyped]) -> Hash[identity, untyped]
27
+ def known: (Array[untyped]) -> Hash[identity, untyped]
28
+
20
29
  # : (Array[untyped]) -> Hash[Symbol, untyped]
21
30
  def payload: (Array[untyped]) -> Hash[Symbol, untyped]
22
31
  end
@@ -10,6 +10,9 @@ module PaperTrailDiff
10
10
  # Both the single-record and the batched selectors resolve this here, because
11
11
  # the rule is subtle enough that two copies of it would drift apart.
12
12
  class RootVersionSelection
13
+ # An error is a specification, so it names the way out as well as the wall.
14
+ # A window ending at the present can never gain a later version, which makes
15
+ # "write a checkpoint afterwards" impossible advice on its own.
13
16
  INCOMPLETE: ::String
14
17
 
15
18
  # : (in_range: Array[untyped], selected: Array[untyped], after_range: untyped, windowed: bool, ?context_required: bool, ?filtered: bool, ?live_endpoint: untyped) -> void
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: paper_trail_diff
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.0
4
+ version: 0.7.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Alex Williams
@@ -200,11 +200,11 @@ licenses:
200
200
  metadata:
201
201
  allowed_push_host: https://rubygems.org
202
202
  bug_tracker_uri: https://github.com/aheathwilliams/paper_trail_diff/issues
203
- changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.7.0/CHANGELOG.md
204
- documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.7.0/README.md
203
+ changelog_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.7.1/CHANGELOG.md
204
+ documentation_uri: https://github.com/aheathwilliams/paper_trail_diff/blob/v0.7.1/README.md
205
205
  homepage_uri: https://github.com/aheathwilliams/paper_trail_diff
206
206
  rubygems_mfa_required: 'true'
207
- source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.7.0
207
+ source_code_uri: https://github.com/aheathwilliams/paper_trail_diff/tree/v0.7.1
208
208
  rdoc_options: []
209
209
  require_paths:
210
210
  - lib