Skip to content

fix(normalizr): do not mutate cached journey on result-cache hit#3925

Merged
ntucker merged 5 commits intomasterfrom
fix/journey-mutation-on-result-cache-hit-scalar
Apr 28, 2026
Merged

fix(normalizr): do not mutate cached journey on result-cache hit#3925
ntucker merged 5 commits intomasterfrom
fix/journey-mutation-on-result-cache-hit-scalar

Conversation

@ntucker
Copy link
Copy Markdown
Collaborator

@ntucker ntucker commented Apr 28, 2026

Summary

Companion to #3924 (which targets master), shaped for the argsKey infrastructure on cursor/scalar-schema-design-for-lenses-d6e3 (#3887).

The base PR (#3887) introduced function-typed (argsKey) deps and added a hasStringDeps filter pass to GlobalCache.getResults's hit branch — but kept the pre-existing paths.shift() mutation. That mutation operates on the journey array stored by reference on WeakDependencyMap.Link.journey, so every successive hit on the same cached entry shifts off one more real entity path, progressively corrupting the subscription list returned to consumers.

This PR fixes the bug and (in commit 5a63f74, originally #3928) moves journey materialization to write time so the hit branch returns the stored array by reference with zero post-processing:

@@ packages/normalizr/src/memo/WeakDependencyMap.ts
   set(
     dependencies: Dep<Path, K>[],
     value: V,
     args: readonly any[] = [],
+    /** Optional consumer-facing journey returned to `get()` callers verbatim.
+     * Defaults to `dependencies.map(d => d.path)`. The array becomes a shared
+     * reference held by every subsequent cache hit; callers MUST NOT mutate. */
+    journey?: Path[],
   ) {
     …
-    curLink.journey = dependencies.map(d => d.path) as Path[];
+    curLink.journey = journey ?? (dependencies.map(d => d.path) as Path[]);
   }
@@ packages/normalizr/src/memo/globalCache.ts (getResults)
     if (paths === undefined) {
       data = computeValue();
       paths = this.paths();   // already excludes placeholder + argsKey functions
       this.dependencies[0] = { path: { key: '', pk: '' }, entity: input };
-      this._resultCache.set(this.dependencies, data, this._args);
-    } else {
-      paths.shift();
-      if (this._resultCache.hasStringDeps) {
-        for (let i = 0; i < paths.length; i++) {
-          if (typeof paths[i] === 'function') {
-            paths = paths.filter(p => typeof p !== 'function') as EntityPath[];
-            break;
-          }
-        }
-      }
+      this._resultCache.set(this.dependencies, data, this._args, paths);
     }
+    // hit branch: `paths` aliases the stored Link.journey — return as-is.
     return { data, paths };
   }

The stored journey is built once at write time, in the consumer-facing shape (placeholder slot + function-typed argsKey deps already filtered out by paths()). No mutation, no per-hit allocation, no per-hit typeof === 'function' walk.

Why it matters

Two distinct downstream consumers of the corrupted paths, only one of which is GC-related:

  • Controller.getResponseMetaentityExpiresAt(paths, state.entitiesMeta)expiresAt — picks the earliest entity expiry across the path list. Returns Infinity once the journey is empty. Flows directly into useSuspense (and useCache / useFetch / useDLE / useLive)'s refetch gate if (Date.now() <= expiresAt && !forceFetch) return;, so once any entity is stale the hook never refetches. This bug fires under ImmortalGCPolicy toocreateCountRef is a no-op there, but entityExpiresAt is unconditional.
  • Controller.getResponseMetagcPolicy.createCountRef({key, paths})GCPolicy.entityCount — under-counted entities get reaped while still referenced. This one is GC-only; ImmortalGCPolicy masks it.

Controller.get (used by useQuery) is the GC-only path — it only passes paths to createCountRef.

Empirical confirmation

A tight loop of result-cache hits against the same primed MemoCache drains the journey to zero on the buggy code; with the fix it stays intact:

iterations paths.length (buggy) paths.length (fix)
1 2826 2826
100 2727 2826
1,000 1827 2826
10,000 0 2826

Differences from #3924 (master version)

  • fix(normalizr): do not mutate cached journey on result-cache hit #3924 (master): minimal one-liner paths.shift() → paths = paths.slice(1). No argsKey infrastructure to coexist with.
  • This PR: collapses the existing hasStringDeps filter pass with the placeholder-strip, and additionally moves journey materialization to write time so the hit path is allocation-free.

Test plan

  • Adds regression test in packages/normalizr/src/__tests__/globalCache.test.ts (does not mutate cached journey across repeated result-cache hits) — fails on the buggy version, passes with fix.
  • Adds Controller-level integration test packages/core/src/controller/__tests__/getResponseMeta-countRef.ts simulating three React subscribers to the same endpoint + args; asserts all three countRef closures correctly increment GCPolicy.entityCount for every entity, and that decrement restores counts to zero. Fails on buggy version (under-counts first entity); passes with fix.
  • yarn jest --selectProjects ReactDOM --testPathPatterns "packages/(normalizr|core|endpoint)" — 837 tests pass.
  • yarn jest --selectProjects ReactDOM --testPathPatterns "packages/react/.*useSuspense|useCache|useQuery" — 62 tests pass.

Performance

Methodology: Node 24.14.1, full examples/benchmark normalizr suite × 5 runs per branch + core suite × 3 runs per branch + focused write-only and drain-free fair microbenches. Trimmed mean (drop min/max for n=5; median for n=3). Three branches compared:

  • master — pre-fix, paths.shift() mutation. Cache-hit "throughput" reflects the bug draining the journey: by the time Benchmark.js samples have stabilized, the stored journey is empty and the hit branch returns no work.
  • fix onlypaths.slice(1) / non-mutating hasStringDeps filter at hit time.
  • fix + journey-at-write — this PR's final state (commit 5a63f74). Hit branch is allocation-free.

Read path (cache-hit benchmarks, n=5 trimmed mean, ops/sec)

Benchmark master (buggy) fix only this PR Δ% (this vs master) Δ% (this vs fix only)
denormalizeLong withCache 12,438 11,483 13,291 +6.86% +15.75%
denormalizeLong withCache (Scalar churn) 12,446 11,495 13,277 +6.67% +15.50%
denormalizeLong All withCache 10,087 9,742 11,907 +18.04% +22.23%
denormalizeLong Query-sorted withCache 10,636 9,710 12,313 +15.77% +26.81%
denormalizeLong Scalar withCache (hasStringDeps) 13,961 12,315 13,965 +0.03% +13.40%
denormalizeLong Scalar update withCache (hasStringDeps) 7,600 6,978 7,549 -0.67% +8.19%
denormalizeLong Values withCache 8,878 8,718 9,064 +2.10% +3.97%
denormalizeLongAndShort withEntityCacheOnly 2,979 2,911 3,075 +3.22% +5.63%
query All withCache 10,549 9,269 12,277 +16.38% +32.46%
queryShort 500x withCache 5,601 5,337 5,504 -1.73% +3.14%
denormalizeShort 500x withCache 13,157 13,417 13,217 +0.46% -1.49%

The *withCache benchmarks against master look modest because master was cheating: its paths.shift() was mutating the stored journey on each hit, so Benchmark.js samples were measuring the cost of returning empty subscription lists (after the journey drained to zero), not the real per-hit work. The fix-only column shows what doing the correct amount of work consistently costs. Moving journey materialization to write time recovers that and exceeds master's apparent throughput on every hit-path benchmark, by removing the per-hit slice(1) / filter entirely.

Write path (write-then-discard benchmarks)

These benches do new MemoCache().denormalize(...) per iteration — exactly the "write but no hit" pattern. The optimization moves one allocation rather than adding work, so deltas are within run-to-run noise:

Benchmark fix only this PR Δ% (this vs fix only)
denormalizeLong (1 write, N≈2826) 538 537 -0.06%
denormalizeLong Values (1 write, N≈2826) 477 476 -0.14%
denormalizeLong with mixin Entity 508 515 +1.31%
denormalize bidirectional 50 (1 write, cycles) 9,783 9,599 -1.87%
denormalizeShort 500x (500 fresh writes/iter) 1,404 1,380 -1.71%

Dedicated write-only microbench (one fresh MemoCache, exactly one denormalize per iter, n=5 trimmed mean):

Benchmark fix only this PR Δ%
WRITE-only: ProjectSchema (1 miss) 712 700 -1.64%
WRITE-only: AllProjects (1 miss) 711 708 -0.33%
WRITE-only: StockSchema (1 miss, hasStringDeps) 656 678 +3.30%

The optimization eliminates one redundant dependencies.map(d => d.path) allocation per top-level cache miss (the journey for the result cache was being computed twice — once as paths() for the consumer, once inside WeakDependencyMap.set). On entity-only schemas this is net-zero in the noise; on hasStringDeps it's a small win because the second dependencies.map was building a journey that included function entries which then got skipped at every hit. The inner per-entity cache (_getCache.set) is unchanged.

core suite (n=3, fresh-thermal re-run)

All within run-to-run noise (1–3% per packages/normalizr/AGENTS.md):

Benchmark fix only this PR Δ%
getResponse 10,189 10,315 +1.24%
getResponse Collection 8,173 7,982 -2.34%
get Collection 7,628 7,699 +0.93%
get Query-sorted 7,662 8,997 +17.42%
getSmallResponse 7,694 7,594 -1.30%
getSmallInferredResponse 5,411 5,509 +1.81%
setLong 780 789 +1.15%
setLongWithMerge 448 451 +0.67%
setLongWithSimpleMerge 479 482 +0.63%
setSmallResponse 500x 1,667 1,692 +1.50%

^set* benchmarks don't go through MemoCache.set (it's only invoked from the denormalize cache-miss branch), so they don't move.

Drain-free fair microbench (one fresh MemoCache per iter → 1 miss + 1 hit)

Apples-to-apples since neither branch's bug can accumulate state across iterations:

Benchmark master fix only this PR Δ% (this vs master)
ProjectSchema fresh-prime + 1 hit 649 639 650 +0.15%
AllProjects fresh-prime + 1 hit 664 648 676 +1.81%
StockSchema fresh-prime + 1 hit (hasStringDeps) 597 608 635 +6.37%

V8 deopt analysis

node --trace-opt --trace-deopt on the denormalizeLong slice. Both getResults and paths reach stable TURBOFAN_JS optimization on every branch with zero deopts:

Metric master fix only this PR
Total deopt events 68 71 71
Deopts on getResults 0 0 0
Deopts on paths 0 0 0
Stable tier for getResults TURBOFAN_JS TURBOFAN_JS TURBOFAN_JS

Same set of pre-existing deopting functions across all branches (unvisit, getEntity, denormalize, visit, mergeEntity, arrayEach, etc.) — none introduced or removed by this PR. The simpler hit-branch shape on the final state lets getResults stabilize with fewer recompile cycles than master (22 vs 32 events through MAGLEV/TURBOFAN_JS).

Bundlesize

Net source change in packages/normalizr/src/memo/: the entire if (hasStringDeps) { …filter… } else { paths.slice(1) } block in getResults is removed (–14 LOC); WeakDependencyMap.set gains one optional parameter wrapped in JSDoc (+12 LOC). Roughly –80 bytes minified (CI compressed-size workflow on the rebase commit reported -37 B (-0.05%) on rdcClient.js for the fix-only state; the journey-at-write commit drops it slightly more).

Changeset

Included as .changeset/fix-journey-mutation.md. Version-linked packages: normalizr, core, endpoint, rest, graphql, react.


Note

Medium Risk
Touches core memoization/caching behavior used by all denormalize reads; while the change is small, it affects subscription paths used for GC ref-counting and expiry calculations and could surface subtle regressions in cache semantics.

Overview
Prevents GlobalCache.getResults from mutating cached dependency "journeys" on result-cache hits by materializing the consumer-facing paths once on cache write and storing it as the WeakDependencyMap journey returned verbatim on subsequent hits.

Adds regression coverage in normalizr and core to ensure repeated reads keep stable paths, expiresAt, and GCPolicy countRef tracking across multiple subscribers, and bumps @data-client/react peer ranges in @data-client/img and @data-client/test to include ^0.17.0.

Reviewed by Cursor Bugbot for commit 05e1298. Bugbot is set up for automated code reviews on this repo. Configure here.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 28, 2026

🦋 Changeset detected

Latest commit: 05e1298

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
@data-client/normalizr Patch
@data-client/core Patch
@data-client/endpoint Patch
@data-client/rest Patch
@data-client/graphql Patch
@data-client/react Patch
@data-client/img Patch
@data-client/test Patch
example-benchmark Patch
normalizr-github-example Patch
normalizr-redux-example Patch
normalizr-relationships Patch
@data-client/vue Patch
example-benchmark-react Patch
test-bundlesize Patch
coinbase-lite Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@vercel
Copy link
Copy Markdown

vercel Bot commented Apr 28, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs-site Ignored Ignored Preview Apr 28, 2026 1:08pm

Request Review

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 28, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.21%. Comparing base (84078d7) to head (05e1298).
⚠️ Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #3925      +/-   ##
==========================================
- Coverage   98.21%   98.21%   -0.01%     
==========================================
  Files         154      154              
  Lines        3024     3018       -6     
  Branches      605      604       -1     
==========================================
- Hits         2970     2964       -6     
  Misses         11       11              
  Partials       43       43              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Base automatically changed from cursor/scalar-schema-design-for-lenses-d6e3 to master April 28, 2026 05:10
@ntucker ntucker force-pushed the fix/journey-mutation-on-result-cache-hit-scalar branch from ce58191 to fc6aeba Compare April 28, 2026 05:16
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark React

Details
Benchmark suite Current: 05e1298 Previous: 84078d7 Ratio
data-client: getlist-100 132.46 ops/s (± 4.6%) 142.86 ops/s (± 5.0%) 1.08
data-client: getlist-500 37.04 ops/s (± 6.6%) 42.38 ops/s (± 6.4%) 1.14
data-client: update-entity 307.77 ops/s (± 9.8%) 377.49 ops/s (± 10.2%) 1.23
data-client: update-user 307.77 ops/s (± 7.0%) 370.37 ops/s (± 9.8%) 1.20
data-client: getlist-500-sorted 40.49 ops/s (± 7.8%) 44.95 ops/s (± 6.8%) 1.11
data-client: update-entity-sorted 256.41 ops/s (± 5.8%) 312.5 ops/s (± 4.9%) 1.22
data-client: update-entity-multi-view 274.02 ops/s (± 6.0%) 333.33 ops/s (± 10.7%) 1.22
data-client: list-detail-switch-10 6.45 ops/s (± 6.4%) 7.18 ops/s (± 5.8%) 1.11
data-client: update-user-10000 78.43 ops/s (± 11.6%) 95.7 ops/s (± 12.4%) 1.22
data-client: invalidate-and-resolve 33.56 ops/s (± 4.8%) 39.22 ops/s (± 5.3%) 1.17
data-client: unshift-item 200 ops/s (± 4.8%) 243.9 ops/s (± 4.3%) 1.22
data-client: delete-item 263.16 ops/s (± 5.6%) 294.12 ops/s (± 6.8%) 1.12
data-client: move-item 156.25 ops/s (± 9.5%) 196.08 ops/s (± 10.0%) 1.25

This comment was automatically generated by workflow using github-action-benchmark.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented Apr 28, 2026

Size Change: -37 B (-0.05%)

Total Size: 81 kB

📦 View Changed
Filename Size Change
examples/test-bundlesize/dist/rdcClient.js 10.8 kB -37 B (-0.34%)
ℹ️ View Unchanged
Filename Size
examples/test-bundlesize/dist/App.js 1.46 kB
examples/test-bundlesize/dist/polyfill.js 307 B
examples/test-bundlesize/dist/rdcEndpoint.js 8.03 kB
examples/test-bundlesize/dist/react.js 59.7 kB
examples/test-bundlesize/dist/webpack-runtime.js 726 B

compressed-size-action

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Benchmark

Details
Benchmark suite Current: 05e1298 Previous: 84078d7 Ratio
normalizeLong 429 ops/sec (±4.04%) 433 ops/sec (±2.62%) 1.01
normalizeLong Values 400 ops/sec (±0.32%) 395 ops/sec (±0.27%) 0.99
normalizeLong Scalar 329 ops/sec (±0.50%) 331 ops/sec (±0.85%) 1.01
normalizeLong Scalar update 837 ops/sec (±0.49%) 853 ops/sec (±0.69%) 1.02
denormalizeLong 236 ops/sec (±4.70%) 243 ops/sec (±4.33%) 1.03
denormalizeLong Values 218 ops/sec (±4.48%) 231 ops/sec (±4.22%) 1.06
denormalizeLong donotcache 960 ops/sec (±0.68%) 942 ops/sec (±0.13%) 0.98
denormalizeLong Values donotcache 712 ops/sec (±0.41%) 717 ops/sec (±0.20%) 1.01
denormalizeLong Scalar donotcache 894 ops/sec (±1.77%) 902 ops/sec (±0.59%) 1.01
denormalizeShort donotcache 500x 1533 ops/sec (±0.70%) 1540 ops/sec (±0.10%) 1.00
denormalizeShort 500x 697 ops/sec (±4.13%) 717 ops/sec (±3.96%) 1.03
denormalizeShort 500x withCache 7055 ops/sec (±0.17%) 6899 ops/sec (±0.14%) 0.98
queryShort 500x withCache 2685 ops/sec (±0.61%) 2819 ops/sec (±0.17%) 1.05
buildQueryKey All 54261 ops/sec (±0.78%) 53631 ops/sec (±0.51%) 0.99
query All withCache 5300 ops/sec (±0.24%) 6245 ops/sec (±0.31%) 1.18
denormalizeLong with mixin Entity 227 ops/sec (±4.04%) 233 ops/sec (±3.82%) 1.03
denormalizeLong withCache 7019 ops/sec (±0.19%) 6104 ops/sec (±0.38%) 0.87
denormalizeLong withCache (Scalar churn) 7007 ops/sec (±0.18%) 6090 ops/sec (±0.23%) 0.87
denormalizeLong Values withCache 4929 ops/sec (±0.31%) 5139 ops/sec (±0.17%) 1.04
denormalizeLong Scalar withCache 7761 ops/sec (±1.42%) 7615 ops/sec (±2.80%) 0.98
denormalizeLong Scalar update withCache 4068 ops/sec (±0.42%) 4011 ops/sec (±1.12%) 0.99
denormalizeLong All withCache 6122 ops/sec (±0.19%) 6420 ops/sec (±0.23%) 1.05
denormalizeLong Query-sorted withCache 5379 ops/sec (±0.21%) 6232 ops/sec (±0.21%) 1.16
denormalizeLongAndShort withEntityCacheOnly 1669 ops/sec (±0.83%) 1659 ops/sec (±0.21%) 0.99
denormalize bidirectional 50 4922 ops/sec (±4.58%) 5090 ops/sec (±4.89%) 1.03
denormalize bidirectional 50 donotcache 40019 ops/sec (±0.21%) 36895 ops/sec (±0.16%) 0.92
getResponse 4586 ops/sec (±0.87%) 4636 ops/sec (±0.97%) 1.01
getResponse (null) 10060925 ops/sec (±0.63%) 9348305 ops/sec (±1.09%) 0.93
getResponse (clear cache) 223 ops/sec (±3.99%) 227 ops/sec (±4.11%) 1.02
getSmallResponse 3425 ops/sec (±0.25%) 3556 ops/sec (±0.35%) 1.04
getSmallInferredResponse 2558 ops/sec (±0.08%) 2623 ops/sec (±0.25%) 1.03
getResponse Collection 4559 ops/sec (±0.48%) 4566 ops/sec (±0.65%) 1.00
get Collection 4404 ops/sec (±0.30%) 4607 ops/sec (±0.27%) 1.05
get Query-sorted 5204 ops/sec (±0.26%) 5287 ops/sec (±0.40%) 1.02
setLong 440 ops/sec (±0.38%) 431 ops/sec (±0.19%) 0.98
setLongWithMerge 252 ops/sec (±0.52%) 250 ops/sec (±0.34%) 0.99
setLongWithSimpleMerge 268 ops/sec (±0.66%) 266 ops/sec (±0.46%) 0.99
setSmallResponse 500x 939 ops/sec (±0.07%) 921 ops/sec (±0.68%) 0.98

This comment was automatically generated by workflow using github-action-benchmark.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fc6aeba. Configure here.

Comment thread packages/endpoint/src/schemas/Scalar.ts Outdated
* pkg: Bump peerdeps of @data-client/react to support 0.17

* pkg: Update yarn.lock for peerdep bump
Move the per-hit paths.slice(1) (and the hasStringDeps filter loop)
out of GlobalCache.getResults and into the cache write.

GlobalCache.paths() already produces the placeholder-free,
function-free shape every consumer needs. Hand it to
WeakDependencyMap.set as the journey, and the cache-hit branch can
return that array by reference - no per-hit allocation, no per-hit
typeof === 'function' walk.

Safety: paths is now a shared reference held by every subsequent
hit. The contract that consumers must not mutate it was established
by the journey-mutation fix (PR #3925) and is exercised by the
existing globalCache.test.ts regression test.
…#3929)

* test(core): replace getResponseMeta-countRef with getResponseMeta-paths

The previous integration test poked GCPolicy['entityCount'] directly to
prove the journey-mutation bug. Replace it with a test that asserts the
public-API consequence the bug creates outside of GC: every subscriber
to the same endpoint must observe the same expiresAt from
Controller.getResponseMeta().

This is the property the bug actually broke for non-GC users: with the
buggy paths.shift(), entityExpiresAt(paths, …) iterates a
progressively-shorter list, dropping the entity with the earliest
expiry first. Subscriber 2 observes a too-late expiresAt; subscriber 3+
observe Infinity and never refetch. Fires under ImmortalGCPolicy too,
since entityExpiresAt is unconditional whenever the endpoint has no
top-level meta.expiresAt — typical for state populated via
controller.set(Entity, …), SSR hydration, or useQuery.

Verified the assertion fails on the buggy paths.shift() (m3.expiresAt
returns FOO_2_EXPIRY instead of FOO_1_EXPIRY) and passes on the fix.

* test(core): keep existing countRef integration test alongside paths test

Restore the GC-side getResponseMeta-countRef.ts integration test that
the prior commit replaced. The two tests cover the journey-mutation
bug from complementary angles:

- getResponseMeta-countRef.ts: GC consumer of paths (entityCount
  under-counting → premature reaping under default GCPolicy).
- getResponseMeta-paths.ts: non-GC consumer of paths
  (entityExpiresAt → suppressed entity-expiry refetch; fires under
  ImmortalGCPolicy too).

Both pass on the fix; both fail on the buggy paths.shift().
@ntucker ntucker merged commit 6e8e499 into master Apr 28, 2026
27 checks passed
@ntucker ntucker deleted the fix/journey-mutation-on-result-cache-hit-scalar branch April 28, 2026 13:16
This was referenced Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant