Skip to content

chore(davinci-client): refactor node reducer updater to remove throws - #753

Open
ancheetah wants to merge 1 commit into
mainfrom
SDKS-5177-refactor-reducer
Open

chore(davinci-client): refactor node reducer updater to remove throws#753
ancheetah wants to merge 1 commit into
mainfrom
SDKS-5177-refactor-reducer

Conversation

@ancheetah

@ancheetah ancheetah commented Aug 10, 2026

Copy link
Copy Markdown
Collaborator

JIRA Ticket

https://pingidentity.atlassian.net/browse/SDKS-5177

Description

What

Refactors collector-value validation in davinci-client so node.reducer.ts no longer throws on invalid updates, moving that validation into a single shared function used by both client.store.ts's update() method and the reducer. update() also becomes lazy: it now defers collector-state lookup and validation until the returned updater function is invoked, rather than at call time.

Changes

  • Added resolveCollectorUpdateValue (client.store.utils.ts), an Effect Match-based validator that is the single source of truth for the collector → accepted-value-type mapping. Returns an Either instead of throwing.
  • Added isValidCollectorCategory, a reusable type guard for narrowing a Collectors union member by category.
  • node.reducer.ts's node/update case no longer throws; every unmatched/invalid branch is now a documented no-op, since update() already validates before dispatching. Added explicit returns after each branch (some previously relied on fallthrough).
  • client.store.ts's update() now returns its validation-and-dispatch logic entirely inside the returned updater function (previously it validated eagerly when update(collector) was called, then returned a dispatch-only closure). Failure paths now log via log.error before returning an InternalErrorResponse, consistent with validate().
  • Added UpdatableCollectors type (client.types.ts) to replace the repeated SingleValueCollectors | MultiSelectCollector | ObjectValueCollectors | AutoCollectors union used by both update() and Updater<T>.
  • Widened CollectorValueType for MultiSelectCollector/MultiValueCollector from string[] to string | string[], matching the reducer's existing support for pushing a single string value.
  • Added CollectorCategory type alias (node.types.ts).

Summary by CodeRabbit

  • New Features

    • Added support for continuing flow states with a dedicated "continue" status.
    • Multi-select values now accept either a single string or an array of strings.
    • Added clearer public type definitions for collector categories and updateable collectors.
    • Added validation for collector categories, values, required properties, options, and device-related errors.
  • Bug Fixes

    • Invalid, unsupported, or unmatched collector updates now safely do nothing instead of throwing errors.

@changeset-bot

changeset-bot Bot commented Aug 10, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: fc3136f

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

This PR includes changesets to release 12 packages
Name Type
@forgerock/davinci-client Patch
@forgerock/device-client Patch
@forgerock/journey-client Patch
@forgerock/oidc-client Patch
@forgerock/protect Patch
@forgerock/sdk-types Patch
@forgerock/sdk-utilities Patch
@forgerock/iframe-manager Patch
@forgerock/sdk-logger Patch
@forgerock/sdk-oidc Patch
@forgerock/sdk-request-middleware Patch
@forgerock/storage 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

@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR updates DaVinci collector update types, centralizes collector validation, changes invalid reducer updates to no-ops, updates related tests, and adds a patch-release changeset.

Changes

Collector update refactor

Layer / File(s) Summary
Public update contracts
packages/davinci-client/src/lib/node.types.ts, packages/davinci-client/src/lib/client.types.ts, packages/davinci-client/api-report/*, packages/davinci-client/src/lib/client.types.test-d.ts
Adds CollectorCategory and UpdatableCollectors, allows string or string-array multi-select values, constrains Updater, and updates continuing-node status contracts.
Shared validation and store updates
packages/davinci-client/src/lib/client.store.utils.ts, packages/davinci-client/src/lib/client.store.ts
Adds category and collector-value validation. The store validates updates before dispatch and returns structured errors for invalid inputs.
Reducer no-op behavior
packages/davinci-client/src/lib/node.reducer.ts, packages/davinci-client/src/lib/node.reducer.test.ts, .changeset/five-badgers-rule.md
The reducer ignores missing, read-only, invalid, and unmatched updates. Tests now verify unchanged state instead of thrown errors. A patch changeset documents the refactor.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant ClientStore
  participant ValidationUtils
  participant NodeReducer
  Client->>ClientStore: submit collector update
  ClientStore->>ValidationUtils: validate category and value
  ValidationUtils-->>ClientStore: narrowed value or error
  ClientStore->>NodeReducer: dispatch valid update
  NodeReducer-->>ClientStore: updated state or unchanged state
Loading

Possibly related PRs

Suggested reviewers: ryanbas21

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main change: refactoring the DaVinci node reducer updater to remove throws.
Description check ✅ Passed The description includes the Jira ticket, detailed changes, validation behavior, lazy updates, type changes, and changeset context.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Create stacked PR
  • Commit on current branch
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SDKS-5177-refactor-reducer

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ancheetah
ancheetah force-pushed the SDKS-5177-refactor-reducer branch from 0eecf39 to fc3136f Compare August 10, 2026 22:22
@ancheetah
ancheetah marked this pull request as ready for review August 10, 2026 22:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (5)
packages/davinci-client/src/lib/node.reducer.ts (1)

226-237: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Hoist getUpdateValue to module scope.

The reducer re-creates this function on every node/update action. It closes over nothing from the case body. Declare it once at module level next to the reducer.

♻️ Proposed refactor

Add above nodeCollectorReducer:

/**
 * Validates `value` against `collector` and returns the narrowed value, or
 * `null` if validation failed. Discards the validation error: the
 * reducer only needs to know whether to no-op, not why.
 */
function getUpdateValue<T extends UpdatableCollectors>(
  collector: T,
  value: CollectorValueTypes,
): CollectorValueType<T> | null {
  const result = resolveCollectorUpdateValue(collector, value);
  return Either.isLeft(result) ? null : result.right;
}

Then remove the inner declaration:

-      /**
-       * Validates `value` against `collector` and returns the narrowed value, or
-       * `null` if validation failed. Discards the validation error: the
-       * reducer only needs to know whether to no-op, not why.
-       */
-      function getUpdateValue<T extends UpdatableCollectors>(
-        collector: T,
-        value: CollectorValueTypes,
-      ): CollectorValueType<T> | null {
-        const result = resolveCollectorUpdateValue(collector, value);
-        return Either.isLeft(result) ? null : result.right;
-      }
-
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/node.reducer.ts` around lines 226 - 237, Move
the standalone getUpdateValue function from the node/update case body to module
scope alongside nodeCollectorReducer, preserving its existing generic signature,
validation logic, and documentation. Remove the inner declaration so the reducer
reuses the module-level helper.
packages/davinci-client/src/lib/node.reducer.test.ts (1)

357-386: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add no-op coverage for invalid values on other updatable categories.

These tests cover the category gate and the MetadataCollector value check. The new resolveCollectorUpdateValue failure paths for the other categories are untested in this reducer. Add cases such as a boolean value sent to a TextCollector and an object value sent to a MultiSelectCollector, and assert the state stays unchanged.

Also applies to: 388-429, 431-456, 2396-2414

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/node.reducer.test.ts` around lines 357 - 386,
Extend the reducer tests around nodeCollectorReducer with no-op cases for
invalid values across the remaining updatable collector categories, including a
boolean value for a TextCollector and an object value for a
MultiSelectCollector. Reuse the existing collector fixtures and action structure
where appropriate, and assert each invalid update returns state unchanged to
cover the resolveCollectorUpdateValue failure paths.
packages/davinci-client/src/lib/client.store.utils.ts (2)

113-128: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚖️ Poor tradeoff

Move the logging effect out of this *.utils.ts file.

handleUpdateValidateError invokes cb(message) at line 118, so calling it writes a log entry. That makes the utility effectful. Keep *.utils.ts pure, and place the single logging effect in an *.effects.ts module, or let each caller log before it returns the error response.

As per coding guidelines: "Keep *.utils.ts files pure and stateless; never put effectful logic in them. Place single isolated effects in *.effects.ts or multi-step workflows in *.micros.ts".

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/client.store.utils.ts` around lines 113 -
128, Remove the cb(message) side effect from handleUpdateValidateError so the
utility only constructs and returns the InternalErrorResponse. Move the single
logging call into an appropriate effects module or into each caller immediately
before returning the validation error, preserving one log entry per error.

Source: Coding guidelines


169-259: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Add a fallback for unmatched collectors.

Match.exhaustive throws when runtime input bypasses TypeScript's exhaustiveness checks. Although current callers use isValidCollectorCategory, the exported resolveCollectorUpdateValue can receive an unmatched collector. Return err('Collector does not fall into a category that can be updated') with Match.orElse instead.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/client.store.utils.ts` around lines 169 -
259, Update resolveCollectorUpdateValue’s Match chain by replacing
Match.exhaustive with Match.orElse that returns err('Collector does not fall
into a category that can be updated') for unmatched runtime collectors, while
preserving all existing collector-specific validation branches.
packages/davinci-client/src/lib/client.types.test-d.ts (1)

182-182: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Replace toMatchTypeOf with toExtend. The pinned Vitest version resolves expect-type 1.2.2, which deprecates toMatchTypeOf. toExtend<CollectorValueTypes>() preserves this assignability check; the assertion remains weaker than the previous exact-equality check.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/client.types.test-d.ts` at line 182, In the
updater type assertion, replace the deprecated toMatchTypeOf call with toExtend
while retaining CollectorValueTypes as the generic type, preserving the existing
assignability check.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/davinci-client/src/lib/node.reducer.ts`:
- Around line 259-271: Update the MultiValueCollector branch in the reducer to
honor action.payload.index when applying scalar updates, inserting or replacing
the value at the specified position as required by the Updater contract while
preserving full-array replacement behavior. If indexed updates are not
supported, instead remove index consistently from the Updater signature and
action payload.

---

Nitpick comments:
In `@packages/davinci-client/src/lib/client.store.utils.ts`:
- Around line 113-128: Remove the cb(message) side effect from
handleUpdateValidateError so the utility only constructs and returns the
InternalErrorResponse. Move the single logging call into an appropriate effects
module or into each caller immediately before returning the validation error,
preserving one log entry per error.
- Around line 169-259: Update resolveCollectorUpdateValue’s Match chain by
replacing Match.exhaustive with Match.orElse that returns err('Collector does
not fall into a category that can be updated') for unmatched runtime collectors,
while preserving all existing collector-specific validation branches.

In `@packages/davinci-client/src/lib/client.types.test-d.ts`:
- Line 182: In the updater type assertion, replace the deprecated toMatchTypeOf
call with toExtend while retaining CollectorValueTypes as the generic type,
preserving the existing assignability check.

In `@packages/davinci-client/src/lib/node.reducer.test.ts`:
- Around line 357-386: Extend the reducer tests around nodeCollectorReducer with
no-op cases for invalid values across the remaining updatable collector
categories, including a boolean value for a TextCollector and an object value
for a MultiSelectCollector. Reuse the existing collector fixtures and action
structure where appropriate, and assert each invalid update returns state
unchanged to cover the resolveCollectorUpdateValue failure paths.

In `@packages/davinci-client/src/lib/node.reducer.ts`:
- Around line 226-237: Move the standalone getUpdateValue function from the
node/update case body to module scope alongside nodeCollectorReducer, preserving
its existing generic signature, validation logic, and documentation. Remove the
inner declaration so the reducer reuses the module-level helper.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 7d0697d4-1d15-4665-b68e-0db06f1c4295

📥 Commits

Reviewing files that changed from the base of the PR and between 9afa636 and fc3136f.

📒 Files selected for processing (10)
  • .changeset/five-badgers-rule.md
  • packages/davinci-client/api-report/davinci-client.api.md
  • packages/davinci-client/api-report/davinci-client.types.api.md
  • packages/davinci-client/src/lib/client.store.ts
  • packages/davinci-client/src/lib/client.store.utils.ts
  • packages/davinci-client/src/lib/client.types.test-d.ts
  • packages/davinci-client/src/lib/client.types.ts
  • packages/davinci-client/src/lib/node.reducer.test.ts
  • packages/davinci-client/src/lib/node.reducer.ts
  • packages/davinci-client/src/lib/node.types.ts

Comment on lines 259 to 271
if (collector.category === 'MultiValueCollector') {
if (typeof action.payload.value !== 'string' && !Array.isArray(action.payload.value)) {
throw new Error('MultiValueCollector does not accept an object');
const value = getUpdateValue(collector, action.payload.value);
if (value === null) {
return;
}
if (Array.isArray(action.payload.value)) {
collector.input.value = [...action.payload.value];

if (Array.isArray(value)) {
collector.input.value = [...value];
} else {
collector.input.value.push(action.payload.value);
collector.input.value.push(value);
}
return;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Find every producer and consumer of the update payload `index` field.
set -euo pipefail

fd -e ts -e tsx . packages/davinci-client/src | xargs rg -n -C4 '\bindex\b' -g '!*.test.ts' \
  | rg -n -C4 'payload|Updater|update\(' || true

# Show the full node/update reducer case for confirmation.
ast-grep run --pattern 'addCase(updateCollectorValues, ($$$) => { $$$ })' --lang typescript packages/davinci-client/src/lib/node.reducer.ts

Repository: ForgeRock/ping-javascript-sdk

Length of output: 4726


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- updater type and implementation ---'
sed -n '110,140p' packages/davinci-client/src/lib/client.types.ts
sed -n '320,375p' packages/davinci-client/src/lib/client.store.ts

printf '%s\n' '--- reducer action and relevant branches ---'
sed -n '40,85p' packages/davinci-client/src/lib/node.reducer.ts
sed -n '225,280p' packages/davinci-client/src/lib/node.reducer.ts

printf '%s\n' '--- update action declarations and all index references ---'
rg -n -C3 '\bindex\b|updateCollectorValues|nodeSlice\.actions\.update' packages/davinci-client/src/lib \
  -g '*.ts' -g '!*.test.ts' -g '!*.test-d.ts'

printf '%s\n' '--- multi-value collector shape and update-value helper ---'
rg -n -C5 'MultiValueCollector|getUpdateValue|resolveCollectorUpdateValue' packages/davinci-client/src/lib \
  -g '*.ts' -g '!*.test.ts' -g '!*.test-d.ts'

Repository: ForgeRock/ping-javascript-sdk

Length of output: 50387


🏁 Script executed:

#!/bin/bash
set -euo pipefail

python3 - <<'PY'
from pathlib import Path
p = Path("packages/davinci-client/src/lib/node.reducer.ts")
text = p.read_text()
start = text.index("if (collector.category === 'MultiValueCollector')")
end = text.index("\n      if (collector.type === 'DeviceAuthenticationCollector')", start)
branch = text[start:end]
print("--- MultiValueCollector reducer branch ---")
print(branch)
print("--- payload.index references in branch ---")
print(branch.count("action.payload.index"))
PY

printf '%s\n' '--- tests and documentation for updater index semantics ---'
rg -n -C4 'index|MultiValueCollector|update\(' packages/davinci-client/src/lib \
  -g '*.test.ts' -g '*.test-d.ts' -g '*.md' \
  | head -n 240

printf '%s\n' '--- multi-value factory and input initialization ---'
sed -n '700,760p' packages/davinci-client/src/lib/collector.utils.ts
rg -n -C4 'MultiValueCollector' packages/davinci-client/src/lib/collector.types.ts

Repository: ForgeRock/ping-javascript-sdk

Length of output: 13675


Handle action.payload.index for MultiValueCollector updates. The public Updater contract exposes index, but the reducer ignores it and always appends scalar values. If index is obsolete, remove it from the Updater signature and action payload.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/davinci-client/src/lib/node.reducer.ts` around lines 259 - 271,
Update the MultiValueCollector branch in the reducer to honor
action.payload.index when applying scalar updates, inserting or replacing the
value at the specified position as required by the Updater contract while
preserving full-array replacement behavior. If indexed updates are not
supported, instead remove index consistently from the Updater signature and
action payload.

])
) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

If we do this check at the store.ts level, do we need to do it here too?

collector.input.value = [...value];
} else {
collector.input.value.push(action.payload.value);
collector.input.value.push(value);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this considered safe because it uses Immer? I can't recall, i know immer does the mutation but I would have to look up the push.

We could just use a concat if we needed instead.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this considered safe because it uses Immer?

Yes, that is correct. You can see it explained here: https://redux-toolkit.js.org/usage/immer-reducers#redux-toolkit-and-immer. Though, I have no issue with explicitly writing it in with immutable grammar.

return err('Value argument cannot be undefined');
}

return Match.value<UpdatableCollectors>(collector).pipe(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Honestly i really like this.

The only piece that irks me slightly is that my brain tells me we are handling the errors in the wrong place, however it's not a big deal. I think this makes sense.

I also don't mind the ok and err notation here since Result<T, E> = Ok<T> | Err<E> will be the basic type going foward in effect, however in this context it is mixing two different vocabularies for the same underlying structure.

@cerebrl cerebrl left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

I like this, but I do worry about mulling in more of the Effect library due to Match. If we can get some numbers around this for context, that would be great. I very much do like the use of Match though :)

Comment on lines +205 to +208
// Every branch below is a no-op rather than a throw: `update()` in
// client.store.ts already validates the id exists and the category is
// updatable before dispatching, so reaching an unmatched case here means
// the action was dispatched directly, bypassing that gate.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Since these reducers are not accessible via our public API, do we need these checks? I like writing strong, defensive code for our public APIs, but for private methods we call internally, I'm not sure of the value.

collector.input.value = [...value];
} else {
collector.input.value.push(action.payload.value);
collector.input.value.push(value);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Is this considered safe because it uses Immer?

Yes, that is correct. You can see it explained here: https://redux-toolkit.js.org/usage/immer-reducers#redux-toolkit-and-immer. Though, I have no issue with explicitly writing it in with immutable grammar.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

3 participants