feat(sdk): shared store for multiple SDK clients + @forgerock/sdk-store - #729
feat(sdk): shared store for multiple SDK clients + @forgerock/sdk-store#729ryanbas21 wants to merge 8 commits into
Conversation
🦋 Changeset detectedLatest commit: 60ece35 The changes in this PR will be included in the next version bump. This PR includes changesets to release 13 packages
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 |
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
|
View your CI Pipeline Execution ↗ for commit 60ece35
💡 Verify your cache is correct by running tasks in a sandbox. Read docs ↗ ☁️ Nx Cloud last updated this comment at |
@forgerock/davinci-client
@forgerock/device-client
@forgerock/journey-client
@forgerock/oidc-client
@forgerock/protect
@forgerock/sdk-types
@forgerock/sdk-utilities
@forgerock/iframe-manager
@forgerock/sdk-logger
@forgerock/sdk-oidc
@forgerock/sdk-request-middleware
@forgerock/storage
@forgerock/sdk-store
commit: |
|
Deployed 7fe4031 to https://ForgeRock.github.io/ping-javascript-sdk/pr-729/7fe40317567aad3b661067f529ec75ca6ea13ae1 branch gh-pages in ForgeRock/ping-javascript-sdk |
📦 Bundle Size Analysis📦 Bundle Size Analysis🚨 Significant Changes🔻 @forgerock/sdk-oidc - 3.5 KB (-2.2 KB, -38.4%) 🆕 New Packages🆕 @forgerock/sdk-store - 11.5 KB (new) 📊 Minor Changes📈 @forgerock/davinci-client - 55.7 KB (+0.6 KB) ➖ No Changes➖ @forgerock/sdk-logger - 1.6 KB 15 packages analyzed • Baseline from latest Legend🆕 New package ℹ️ How bundle sizes are calculated
🔄 Updated automatically on each push to this PR |
Codecov Report❌ Patch coverage is ❌ Your project status has failed because the head coverage (24.08%) is below the target coverage (40.00%). You can increase the head coverage or adjust the target coverage. Additional details and impacted files@@ Coverage Diff @@
## main #729 +/- ##
==========================================
+ Coverage 18.07% 24.08% +6.00%
==========================================
Files 155 165 +10
Lines 24398 25829 +1431
Branches 1203 1709 +506
==========================================
+ Hits 4410 6220 +1810
+ Misses 19988 19609 -379
🚀 New features to boost your workflow:
|
9a87a40 to
aae6ae5
Compare
| * not require a live PingOne endpoint. | ||
| */ | ||
|
|
||
| const WELLKNOWN_URL = 'https://sdk-test.example.com/as/.well-known/openid-configuration'; |
9056cb6 to
34a45be
Compare
Allow davinci(), journey(), and oidc() to share a single Redux store, so
the OpenID Connect discovery document is fetched once regardless of how many
clients are initialised. Three ownership modes are supported:
// Mode 1 — implicit (unchanged default)
const client = await oidc({ config });
// Mode 2 — one client owns the store (primary sharing story)
const dv = await davinci({ config: davinciConfig });
const oc = await oidc({ config: oidcConfig, store: dv.store });
// Mode 3 — consumer owns the store
const store = createSdkStore();
await davinci({ config: davinciConfig, store });
await oidc({ config: oidcConfig, store });
New @forgerock/sdk-store package (scope:sdk-effects) owns:
- The single canonical wellknownApi instance and its RTK Query selectors.
createWellknownSelector was rebuilt on every call; it is now memoized per
URL via a module-level Map, restoring the memoization that was always cold.
- initWellknownQuery and isValidWellknownResponse (moved from sdk-oidc).
- The shared store contract: SdkStore / SdkStoreHandle / createSdkStore /
injectClient / isSdkStoreHandle.
- clientExtra() — the per-client slot resolver used by every *.api.ts.
Request middleware and logger are scoped per client (security fix):
Before, every *.api.ts resolved its middleware and logger from the store's
store-wide thunk extraArgument. On a shared store that extra belongs to the
owning client, so DaVinci middleware ran against AUTHORIZE, PAR,
TOKEN_EXCHANGE, REVOKE, USER_INFO and END_SESSION, and oidc's own logger was
silently discarded. extraArgument is now a registry keyed by each api's
reducerPath; clientExtra() returns only the calling client's slot. A missing
or malformed slot yields empty middleware and an error-level fallback logger —
never another client's values.
Public API changes:
- oidc() takes store as part of its options object (not a positional second
arg), consistent with every other factory in the SDK.
- davinci() and journey() expose store: SdkStore on the returned client.
Both factories also accept store?: SdkStore as input (mode 2 / 3).
- One OIDC client per store: a second oidc() with a different clientId
returns argument_error instead of silently overwriting token state.
- oidc() validates arguments before injecting into a store (RTK inject is
irreversible); a rejected call leaves a caller-owned store unchanged.
- A value that fails isSdkStoreHandle() returns argument_error, not TypeError.
Store contract is declared once structurally:
InjectableStore was declared three times — twice identically and once as a
weaker mirror — joined only by `as unknown as`. The fictional __sdkStoreBrand
required casts on both sides with no compile-time link between them. There
is now one structural interface in sdk-store; producers satisfy it without a
cast, consumers receive it without a cast. The two unavoidable widenings live
in store.effects.ts with documented rationale. Deleted: toSdkStore,
fromSdkStore x3, InjectableStore x3, __sdkStoreBrand, JourneyStore,
injectIntoStore, the requestMiddleware log.warn (obsolete under per-client
scoping), and the two tests that existed only to cover that warning.
Layering enforcement:
sdk-store previously depended on sdk-oidc to access initWellknownQuery,
which violated the scope:sdk-effects constraint that allows only sdk-types
and sdk-utilities. Moving the file removes the only cross-effects dependency
and promotes enforce-module-boundaries from warn to error. All 23 affected
projects lint clean.
Tests:
- sdk-store: 40 tests (was 0; passWithNoTests removed).
- wellknownApi: cache-per-URL, error mapping, selector memoization (instance
identity asserted).
- clientExtra: never returns another client's slot; degrades gracefully on
any malformed extra.
- Middleware isolation: DaVinci middleware does not run against OIDC requests,
proven for both the foreign-slot shape and the old flat shape.
- State-shape assertions: combineSlices keys are now implicit (via slice.name)
rather than literal; pinned so a rename fails here, not in selectors.
- shared-store.test.ts fully rewritten: fake handle removed; all three D1
modes tested against the real factories and real createSdkStore().
- store-lifecycle.test.ts: validate-before-inject; clientId conflict guard.
- store.effects.test.ts: createSdkStore, isSdkStoreHandle, injectClient.
E2E:
- Playwright suite in davinci-suites asserts exactly one .well-known network
request when davinci() and oidc() share a store (mode 2). Requests are
intercepted via page.route() so no live credential is needed.
BREAKING (sdk-oidc): initWellknownQuery and isValidWellknownResponse are
removed from @forgerock/sdk-oidc and are now exported from @forgerock/sdk-store.
Update imports if you were using them directly.
- Export INVALID_STORE_MESSAGE from sdk-store and consume it in davinci, journey, and oidc client factories (removes 4x string duplication) - Add optional `defaults` param to clientExtra; simplify *Extra wrapper functions in davinci.api, journey.api, and oidc.api - Expose `store` in oidc-client return for API symmetry with davinci/journey - Restore 2025 - 2026 copyright in davinci client.types.ts
Extract all structural argument checks from oidc() into a pure, synchronous parseOidcArgs() function. The parser returns a ParsedOidcArgs<T> on success — a narrow type where required config fields are non-optional strings and store is SdkStore | undefined — or a GenericError on failure. This removes scattered guard clauses from the oidc() body and lets the type system carry the proof of validity downstream.
34a45be to
60ece35
Compare
Summary
Allows
davinci(),journey(), andoidc()to share a single Redux store so that OpenID Connect discovery (.well-known/openid-configuration) is fetched once instead of once per client.What's in this PR
New package:
@forgerock/sdk-storeA new
scope:sdk-effectspackage that owns:wellknownApiinstance and discovery cache (previously duplicated in each client package)SdkStore,SdkStoreHandle,createSdkStore(),injectClient()initWellknownQuery,isValidWellknownResponseStore sharing — three ownership modes
All three client factories accept an optional
storeoption. Three patterns are supported:All three factories (
davinci,journey,oidc) now exposestoreon their returned client for API symmetry.Middleware and logging are scoped per client
Each client's
requestMiddlewareandloggerare registered against that client alone. Middleware passed todavinci()orjourney()is never applied to OIDC requests, and vice versa. Both options are honored on a shared store.One OIDC client per store
oidc()mounts at a fixed Redux slice key. Initializing a second OIDC client on the same store with a differentclientIdreturns anargument_errorinstead of silently overwriting the first client's token state. Re-initializing with the sameclientIdis idempotent.Validation before attachment
oidc()validates its arguments before attaching to a store, so a rejected call no longer leaves a caller-provided store in a partially modified state. Passing a non-SDK-store value tostorereturns anargument_errorinstead of throwing.davinci()andjourney()follow the same pattern: thestoreargument is validated before initialization proceeds. An invalid store throws synchronously with a consistent error message (INVALID_STORE_MESSAGE).Bug fixes
injectClientpreviously registered a client's middleware on every call. It now checks for an existing registration and skips if already present.createWellknownSelectorrebuilt its selector on every call so the cache never took effect. Selectors are now memoized per URL via a module-levelMap.Refactor:
parseOidcArgs(oidc-client)All structural argument checks are extracted from
oidc()into a pure, synchronousparseOidcArgs()function that returns a narrowParsedOidcArgs<T>type on success or aGenericErroron failure. Removes scattered guard clauses from theoidc()body and lets the type system carry validity proof downstream.Lint enforcement
The
enforce-module-boundariesESLint rule was promoted fromwarntoerroracross the repository. All packages pass.Breaking changes
@forgerock/sdk-oidc:initWellknownQueryandisValidWellknownResponsemove to@forgerock/sdk-store. Update imports if you were using them directly.@forgerock/sdk-store:createStoreExtrais no longer exported (it was unused internally and not part of the documented API).Testing
@forgerock/sdk-storepackage covering store creation, client injection, type guards, and well-known discovery (40 new tests)davinci-client,oidc-client,journey-client)e2e/davinci-suites/src/shared-store.test.ts) — asserts exactly one.well-knownnetwork request whendavinci()andoidc()share a storedavinci-client,oidc-client,journey-client, andsdk-store