Skip to content

Commit b292860

Browse files
committed
feat: add optional autoExtendExpiration
BREAKING CHANGE: by default it will not extend the expiration time of the session when it is saved. This is to prevent unnecessary saves to the session store. You can use `session.touch()` to extend the expiration time of the session manually.
1 parent d29f221 commit b292860

File tree

7 files changed

+94
-16
lines changed

7 files changed

+94
-16
lines changed

README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,14 @@ app.get('/', async (c, next) => {
9191
return c.html(`<h1>You have visited this page ${ session.get('counter') } times</h1>`)
9292
})
9393

94+
app.get('/read', (c) => {
95+
const session = c.get('session')
96+
session.touch() // Update the session expiration time
97+
return c.json({
98+
counter: session.get('counter')
99+
})
100+
})
101+
94102
Deno.serve(app.fetch)
95103
```
96104

deno.lock

Lines changed: 35 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

deps.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,5 @@ export type { Context, MiddlewareHandler } from 'npm:hono@^4.0.0'
22
export { createMiddleware } from 'npm:hono@^4.0.0/factory'
33
export { getCookie, setCookie } from 'npm:hono@^4.0.0/cookie'
44
export type { CookieOptions } from 'npm:hono@^4.0.0/utils/cookie'
5-
export * as Iron from 'npm:iron-webcrypto@^1.2.1'
5+
export * as Iron from 'npm:iron-webcrypto@^1.2.1'
6+
export { default as hash } from 'npm:hash-object@^5'

scripts/build_npm.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ await build({
2929
},
3030
bugs: {
3131
url: "https://github.com/jcs224/hono_sessions/issues",
32-
},
32+
}
3333
},
3434
// typeCheck: false,
3535
// scriptModule: false,
@@ -42,4 +42,4 @@ await build({
4242
// post build steps
4343
Deno.copyFileSync("LICENSE", "npm/LICENSE");
4444
Deno.copyFileSync("README.md", "npm/README.md");
45-
Deno.copyFileSync("extras/.npmignore", "npm/.npmignore")
45+
Deno.copyFileSync("extras/.npmignore", "npm/.npmignore")

src/Middleware.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
1111
const expireAfterSeconds = options.expireAfterSeconds
1212
const cookieOptions = options.cookieOptions
1313
const sessionCookieName = options.sessionCookieName || 'session'
14+
const autoExtendExpiration = options.autoExtendExpiration ?? false
1415

1516
if (store instanceof CookieStore) {
1617
store.sessionCookieName = sessionCookieName
@@ -27,7 +28,7 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
2728
}
2829

2930
const middleware = createMiddleware(async (c, next) => {
30-
const session = new Session
31+
const session = new Session(expireAfterSeconds)
3132
let sid = ''
3233
let session_data: SessionData | null | undefined
3334
let createNewSession = false
@@ -55,7 +56,9 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
5556
session.setCache(session_data)
5657

5758
if (session.sessionValid()) {
58-
session.reupSession(expireAfterSeconds)
59+
if (autoExtendExpiration) {
60+
session.reupSession()
61+
}
5962
} else {
6063
store instanceof CookieStore ? await store.deleteSession(c) : await store.deleteSession(sid)
6164
createNewSession = true
@@ -83,19 +86,25 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
8386
await store.createSession(sid, defaultData)
8487
}
8588

86-
session.setCache(defaultData)
89+
session.setCache(defaultData, true)
8790
}
8891

8992
if (!(store instanceof CookieStore)) {
9093
setCookie(c, sessionCookieName, encryptionKey ? await encrypt(encryptionKey, sid) : sid, cookieOptions)
9194
}
9295

93-
session.updateAccess()
96+
if (autoExtendExpiration) {
97+
session.updateAccess()
98+
}
9499

95100
c.set('session', session)
96101

97102
await next()
98103

104+
if (session.isStale()) {
105+
session.touch()
106+
}
107+
99108
const shouldDelete = session.getCache()._delete;
100109
const shouldRotateSessionKey = c.get("session_key_rotation") === true;
101110
const storeIsCookieStore = store instanceof CookieStore;
@@ -136,7 +145,8 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
136145
*/
137146
const shouldPersistSession =
138147
!shouldDelete &&
139-
(!shouldRotateSessionKey || storeIsCookieStore);
148+
(!shouldRotateSessionKey || storeIsCookieStore) &&
149+
session.isStale();
140150

141151
if (shouldPersistSession) {
142152
store instanceof CookieStore

src/Session.ts

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { hash } from '../deps.ts'
2+
13
interface SessionDataEntry<T> {
24
value: T,
35
flash: boolean
@@ -14,8 +16,11 @@ export interface SessionData<T = any> {
1416
export class Session<T = any> {
1517

1618
private cache: SessionData<T>
19+
private expiration: number | undefined
20+
private hash: string | null = null
1721

18-
constructor() {
22+
constructor(expiration?: number) {
23+
this.expiration = expiration
1924
this.cache = {
2025
_data: {},
2126
_expire: null,
@@ -24,10 +29,15 @@ export class Session<T = any> {
2429
}
2530
}
2631

27-
setCache(cache_data: SessionData<T>) {
32+
setCache(cache_data: SessionData<T>, isNew: boolean = false) {
33+
this.hash = !isNew ? hash(cache_data) : null
2834
this.cache = cache_data
2935
}
3036

37+
isStale(): boolean {
38+
return !this.hash || this.hash !== hash(this.cache)
39+
}
40+
3141
getCache(): SessionData<T> {
3242
return this.cache
3343
}
@@ -36,12 +46,23 @@ export class Session<T = any> {
3646
this.cache._expire = expiration
3747
}
3848

39-
reupSession(expiration: number | null | undefined) {
40-
if (expiration) {
41-
this.setExpiration(new Date(Date.now() + expiration * 1000).toISOString())
49+
/**
50+
* Extend expiration
51+
*/
52+
reupSession() {
53+
if (this.expiration) {
54+
this.setExpiration(new Date(Date.now() + this.expiration * 1000).toISOString())
4255
}
4356
}
4457

58+
/**
59+
* Extend session expiration and update access time
60+
*/
61+
touch() {
62+
this.reupSession()
63+
this.updateAccess()
64+
}
65+
4566
deleteSession() {
4667
this.cache._delete = true
4768
}
@@ -50,6 +71,9 @@ export class Session<T = any> {
5071
return this.cache._expire == null || Date.now() < new Date(this.cache._expire).getTime()
5172
}
5273

74+
/**
75+
* Update the last accessed time
76+
*/
5377
updateAccess() {
5478
this.cache._accessed = new Date().toISOString()
5579
}

src/SessionOptions.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,6 @@ export default interface SessionOptions {
77
encryptionKey?: string,
88
expireAfterSeconds?: number,
99
cookieOptions?: CookieOptions,
10-
sessionCookieName?: string
11-
}
10+
sessionCookieName?: string,
11+
autoExtendExpiration?: boolean
12+
}

0 commit comments

Comments
 (0)