Skip to content

docs(journeys): document multi-journey persistence patterns and the shared-singleton trap#42

Open
kibertoad wants to merge 3 commits into
mainfrom
docs/persistence-multi-journey-shells
Open

docs(journeys): document multi-journey persistence patterns and the shared-singleton trap#42
kibertoad wants to merge 3 commits into
mainfrom
docs/persistence-multi-journey-shells

Conversation

@kibertoad
Copy link
Copy Markdown
Owner

@kibertoad kibertoad commented May 15, 2026

Summary

Add a "Registering multiple journeys" subsection under Persistence in packages/journeys/README.md. It names the variance trap that shows up when a shell tries to DRY its persistence setup across multiple journeys by typing one shared adapter <unknown, TInput>, and walks through the two safe shapes already used in adopters and in this repo's own examples.

Why

The README currently teaches persistence in the context of a single journey, and the existing examples either persist one journey (customer-onboarding) or declare distinct typed adapters per journey (journey-invoke). Both shapes are safe, but neither warns about what happens when a multi-journey shell author writes the natural-looking shared singleton:

// One persistence to rule them all... or so you think.
export const journeyPersistence: SyncJourneyPersistence<unknown, JourneyKey> =
  createWebStoragePersistence<JourneyKey, unknown>({ ... });

registry.registerJourney(journeyA, { persistence: journeyPersistence });
registry.registerJourney(journeyB, { persistence: journeyPersistence });

This compiles fine on its own but blows the variance check at the registerJourney call site — JourneyDefinition.start(state: TState, ...) puts TState contravariantly, and unknown can't satisfy any concrete state. The compiler error (Type 'unknown' is not assignable to type 'YourJourneyState') points at the persistence arg of registerJourney, not the persistence declaration itself, so the actual fix (don't widen TState to unknown; bind it per journey) is not obvious from the message.

Real-world case: hit this in ava-advisor-connect2 wiring PSR + SARP through a shared sessionStorage adapter. The team's first instinct was to silence the error with registerJourney(journey as never, { persistence }), which masks the symptom on the wrong side and leaves persistence's load(...) => SerializedJourney<unknown> lying to consumers. The right fix — a generic factory or distinct typed adapters per journey — was learnable from examples/react-router/journey-invoke but not flagged anywhere as the recommended shape for multi-journey shells.

What the PR adds

Three explicit subheadings under the new "Registering multiple journeys" section:

  1. Safe — one typed adapter per journey. Points readers at examples/*/journey-invoke/shell/src/persistence.ts as the canonical demonstration.
  2. Safe — a generic factory. Full worked example of makeJourneyPersistence<TInput, TState>() invoked per registerJourney call. The shape used by adopters who want a single source of truth for SSR guards / keyFor / storage selection without per-journey repetition.
  3. Trap — one shared adapter typed <unknown, TInput>. Explains the variance failure and points back to the two safe shapes.

No code changes — none of the existing examples or README snippets land on the anti-pattern, so this is purely additive documentation.

Test plan

  • Re-read the rendered section in GitHub's Markdown preview for typography / ordering.
  • Confirm the makeJourneyPersistence code block type-checks against the current public API surface (no missing imports).
  • Spot-check that the "Safe — one typed adapter per journey" pointer to examples/*/journey-invoke still describes those files accurately.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Documentation
    • Expanded guidance on registering multiple journeys with safe persistence patterns, including typed adapters and factory approaches, plus TypeScript typing best practices to avoid common configuration pitfalls.

Review Change Stack

kibertoad and others added 2 commits May 15, 2026 19:23
Shells registering more than one journey often share a single persistence
setup expression across every `registerJourney` call. The natural first
attempt — a single `<unknown, TInput>`-typed adapter object reused
everywhere — fails the variance check on `JourneyDefinition.start(state:
TState, ...)` with `Type 'unknown' is not assignable to type
'YourJourneyState'`, and the symptom points at the `persistence` arg
rather than the underlying widening.

Add a "Sharing a persistence config across multiple journeys" subsection
under Persistence that names the anti-pattern, explains the variance
failure, and shows the generic-factory recipe (per-journey
`makeJourneyPersistence<TInput, TState>()` invocations) used by adopters
in the wild. Composes with the existing `defineJourneyPersistence` and
`createWebStoragePersistence` paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Audit of the existing examples showed the journey-invoke samples
already document a second valid pattern — N distinct typed adapters,
one per journey — which is arguably the simpler answer when the setup
expression is short. The first draft only taught the generic-factory
shape and risked implying it was the only fix.

Restructure the section into three explicit headings: "Safe — one
typed adapter per journey" (with a pointer at the journey-invoke
examples), "Safe — a generic factory" (the previous content), and
"Trap — one shared adapter typed `<unknown, TInput>`" (the
variance-failure case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 15, 2026

Warning

Rate limit exceeded

@kibertoad has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 44 minutes and 57 seconds before requesting another review.

You’ve run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 8994e824-c73c-4f41-bc47-b15c8b6b623b

📥 Commits

Reviewing files that changed from the base of the PR and between fa1226f and a1508e6.

📒 Files selected for processing (1)
  • packages/journeys/README.md
📝 Walkthrough

Walkthrough

Added a new "Registering multiple journeys" persistence subsection to the README explaining safe patterns for wiring multiple journey adapters, including a typed-per-journey approach and a generic factory pattern, with example code and documentation of a common TypeScript typing trap.

Changes

Multiple Journey Persistence Guidance

Layer / File(s) Summary
Persistence guidance for multiple journeys
packages/journeys/README.md
New subsection documenting two safe patterns for registering multiple journeys: one typed adapter per journey or a generic factory invoked per journey, with example implementation and explanation of a typing trap involving incorrectly reused shared adapters.

Estimated code review effort

🎯 1 (Trivial) | ⏱️ ~3 minutes

Poem

🐰 Where journeys dance and states align,
One adapter per—or factories combine,
Beware the trap of unknown spread,
Type wisely, or persistence bleeds!

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main change: documentation of multi-journey persistence patterns and a TypeScript variance-related trap when using shared adapters.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/persistence-multi-journey-shells

Warning

Review ran into problems

🔥 Problems

Stopped waiting for pipeline failures after 30000ms. One of your pipelines takes longer than our 30000ms fetch window to run, so review may not consider pipeline-failure results for inline comments if any failures occurred after the fetch window. Increase the timeout if you want to wait longer or run a @coderabbit review after the pipeline has finished.


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 and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 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/journeys/README.md`:
- Around line 1664-1700: Remove the duplicated safe-factory block (the repeated
code that defines makeJourneyPersistence and the two registry.registerJourney
calls) and replace it with a short anti-pattern example that demonstrates the
variance failure; show a failing example using createWebStoragePersistence and
SyncJourneyPersistence with a contravariant/wrong TState (e.g. const
sharedPersistence: SyncJourneyPersistence<unknown, AppJourneyKey> =
createWebStoragePersistence<AppJourneyKey, unknown>({...})) and then show
registry.registerJourney(onboardingJourney, { persistence: sharedPersistence })
producing the type error, while keeping the original safe patterns above
(makeJourneyPersistence, createWebStoragePersistence, registry.registerJourney)
intact.
🪄 Autofix (Beta)

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

Run ID: 46d32326-ad23-4ac3-a7af-0986436483ec

📥 Commits

Reviewing files that changed from the base of the PR and between c6838ee and fa1226f.

📒 Files selected for processing (1)
  • packages/journeys/README.md

Comment thread packages/journeys/README.md Outdated
CodeRabbit flagged that the "Trap" subsection's code block was a
copy of the safe-factory example above it instead of the variance
failure it's describing. Replace it with the actual shared
`SyncJourneyPersistence<unknown, AppJourneyKey>` shape and the
two `registerJourney` calls that error on the `persistence` arg.
Also drop the trailing factory-pattern paragraph that duplicated
the one above the trap.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant