-
Notifications
You must be signed in to change notification settings - Fork 2.7k
Bring over RR session code into @remix-run/session package #10797
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Conversation
003723b to
80537fe
Compare
9dc0e74 to
d15afe1
Compare
There was a problem hiding this 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.
62f95fa to
7350dde
Compare
| @@ -0,0 +1 @@ | |||
| export { createFileSessionStorage } from './lib/storage/file-storage.ts' | |||
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
| declare module '@remix-run/session' { | ||
| interface SessionData { | ||
| cartId?: string | ||
| userId?: string |
There was a problem hiding this comment.
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], |
There was a problem hiding this comment.
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
…t of fetch-router in the browser bundle
cbe4632 to
8652d7b
Compare
7350dde to
2d87615
Compare
There was a problem hiding this 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()) |
There was a problem hiding this comment.
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:*", |
There was a problem hiding this comment.
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": { |
There was a problem hiding this comment.
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) | ||
| }) | ||
| }) | ||
|
|
There was a problem hiding this comment.
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:^", |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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) | ||
| } | ||
|
|
There was a problem hiding this comment.
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?
93e76b3 to
b8db250
Compare
|
Ended up starting a fresh PR to clean up some of this commit history after a few rebases onto the breaking changes in |
Stacked on #10796
Adds a new
@remix-run/sessionpackage - brought over from https://github.com/remix-run/react-router/blob/main/packages/react-router/lib/server-runtime/sessions.ts