Skip to content

Conversation

@brophdawg11
Copy link
Contributor

@MichaelDeBoey MichaelDeBoey linked an issue Oct 23, 2025 that may be closed by this pull request
@brophdawg11 brophdawg11 force-pushed the brophdawg11/session branch 2 times, most recently from 9dc0e74 to d15afe1 Compare October 23, 2025 14:16
Copy link
Member

@mjackson mjackson left a comment

Choose a reason for hiding this comment

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

Overall we are headed in the right direction, thanks for the effort here this week! I hope the feedback helps.

@brophdawg11 brophdawg11 force-pushed the brophdawg11/session branch 3 times, most recently from 62f95fa to 7350dde Compare October 29, 2025 19:00
@@ -0,0 +1 @@
export { createFileSessionStorage } from './lib/storage/file-storage.ts'
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I needed to move this to a sub-export so that it didn't get picked up when a client-side API imported @remix-run/fetch-router and @remix-run/fetch-router importing the agnostic session stuff from @remix-run/sesson.

This happened from the bookstore routes.ts:

import { route, formAction, resources } from '@remix-run/fetch-router'

export let routes = route({ ... })

When we tried to run pnpm dev:browser, it would error on the file-storage imports of node:fs

* To be used on any route that mutates the cart
*/
export const ensureCart: Middleware = async ({ session }) => {
createCartIfNotExists(session)
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Middleware used by the cart API routes to ensure we always have a cart available if we're trying to mutate the cart

Copy link
Member

Choose a reason for hiding this comment

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

That's a great pattern.

Comment on lines +4 to +7
declare module '@remix-run/session' {
interface SessionData {
cartId?: string
userId?: string
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Module augmentation to strongly type our session

async add({ storage, formData }) {
// Simulate network latency
await new Promise((resolve) => setTimeout(resolve, 1000))
use: [ensureCart],
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Cart middleware that runs before all cart API handlers

Copy link
Member

@mjackson mjackson left a comment

Choose a reason for hiding this comment

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

Made some nice progress on this one, thanks @brophdawg11 🙏

I made a few comments, I hope they're helpful.

In the router itself, I think I may have led you astray asking you to set the session in context._session. I think we should probably always have a session storage (i.e. don't allow them to use createRouter({ sessionStorage: false }), and default to cookie storage) and setup a real session object every time. This will be cheap to do on requests that do not send a session Cookie header since it's just a new, empty session. No deserialize needed.

Then, on the way out we can just do as we're currently doing. If nobody interacted with the session, don't send the Set-Cookie.

To me, this is the lowest friction we can possibly provide for folks. They will always have a session object that just works, without any setup. It's cheap. And we won't be sending unnecessary cookies if they aren't using it.

Does that make sense?

"@remix-run/node-fetch-server": "workspace:*"
},
"devDependencies": {
"@remix-run/session": "workspace:*",
Copy link
Member

Choose a reason for hiding this comment

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

Should be a regular dep, not a dev dep.

"tsx": "^4.20.6"
},
"peerDependencies": {
"dependencies": {
Copy link
Member

Choose a reason for hiding this comment

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

These should all be peer deps, not regular deps. The strategy with individual packages is that they should all use peer deps with other @remix-run/* packages and use --external when building.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

huh yeah I thought I had done that but maybe a bad rebase flipped it back 👍

assert.equal(handlerCalled, false)
})
})

Copy link
Member

Choose a reason for hiding this comment

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

This file is getting huge. Should we put these in a new sessions.test.ts file? I could probably separate out a few others too, like file-uploads.test.ts and middleware.test.ts

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah I'll pull session tests out for now and we can pull the others out in a separate PR

}
},
"dependencies": {
"@remix-run/cookie": "workspace:^",
Copy link
Member

Choose a reason for hiding this comment

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

I've been using workspace:*. This could also work. I just want to be consistent about how we do it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Looks like we have some of each throughout the repo - I'll use workspace:* for this new stuff and we can do a larger pass separately

Session,
} from './lib/session.ts'

export { createCookieSessionStorage } from './lib/storage/cookie-storage.ts'
Copy link
Member

Choose a reason for hiding this comment

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

The file-storage package uses a pattern where we have separate exports for all implementations, e.g. @remix-run/file-storage/local and @remix-run/file-storage/memory.

I think the same pattern makes sense here. Let's put both cookie and memory storage into their own exports. So we'll have:

  • @remix-run/session/cookie-storage
  • @remix-run/session/file-storage
  • @remix-run/session/memory-storage

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good call - I updated to follow the same pattern here. I think in RR we had a loose rule of only using a separate package when the build/runtime required it (i.e., react-router/dom) but I like the feel of these self contained sub-exports for semi-mutually-exclusive behaviors.

export class Session<Data = SessionData, FlashData = Data> {
#id: string
#map: Map<keyof Data | FlashDataKey<keyof FlashData & string>, unknown>
#status: 'new' | 'clean' | 'dirty' | 'destroyed'
Copy link
Member

Choose a reason for hiding this comment

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

The new status feels redundant to me. clean, dirty, and destroyed make sense. But new feels the same as clean to me in the sense that in neither case does it need to be saved.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah the new only commit session on modifications alleviated the original use for new - removed

/**
* An object of name/value pairs to be used in the session.
*/
export interface SessionData {
Copy link
Member

Choose a reason for hiding this comment

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

This feels like an awkward way to achieve type safety for session.data because there's no way to define different data in different session cookies. We should either come up with a solution that works for N session cookies on a site or just return unknown values out of session.data for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yeah this is carried forward from Remix v1 - happy to ditch it since it's just sort of a TS lie anyway

* To be used on any route that mutates the cart
*/
export const ensureCart: Middleware = async ({ session }) => {
createCartIfNotExists(session)
Copy link
Member

Choose a reason for hiding this comment

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

That's a great pattern.

let cookie = await this.#sessionStorage.destroySession(session)
response.headers.append('Set-Cookie', cookie)
}

Copy link
Contributor

Choose a reason for hiding this comment

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

What do you think about implementing an optional touchSession feature on storage?
In cases where sessions are automatically cleaned up by storage upon expiration (cron, ttl), this would allow the idle timer to be reset without updating the entire content.

The touchSession would only be called if the session status is clean and the storage implements touchSession.
The custom storage backend could also manage a touchTTL to avoid updating the backend each time a session is read.

Sorry if you have already ruled out this strategy and I missed the information. If so, what approach would you take?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Lets land the session stuff in it's current state (largely following the implementation in RR) and then we can consider potential enhancements. Feel free to open a discussion outlining the use case and we can further discuss if something like this would be beneficial there 👍

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes! Thank you for the work already done! 👌

@brophdawg11
Copy link
Contributor Author

Ended up starting a fresh PR to clean up some of this commit history after a few rebases onto the breaking changes in main. Closing this in favor of #10811

@brophdawg11 brophdawg11 closed this Nov 3, 2025
@MichaelDeBoey MichaelDeBoey deleted the brophdawg11/session branch November 4, 2025 09:13
@MichaelDeBoey MichaelDeBoey added pkg:session feat:sessions Sessions related issues labels Nov 5, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

feat:sessions Sessions related issues pkg:session

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add session-middleware to fetch-router Add built-in session support in fetch-router Add session package

5 participants