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?

},
})
let context = new RequestContext(req)
let context = new RequestContext(req, createSession())
Copy link
Member

Choose a reason for hiding this comment

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

We don't need this line.

"@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.

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

}
},
"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.

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

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.

/**
* 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.

* 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?

@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
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.

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

5 participants