Skip to content

Commit 3c29d0e

Browse files
authored
Merge pull request #30 from jfromaniello/prevent_unnecesary_saves
feat: prevent unnecessary cookie overwrites and add manual session touch
2 parents 7fee5ec + b292860 commit 3c29d0e

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
@@ -93,6 +93,14 @@ app.get('/', async (c, next) => {
9393
return c.html(`<h1>You have visited this page ${ session.get('counter') } times</h1>`)
9494
})
9595

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

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 (options.encryptionKey !== undefined) {
1617
if (typeof options.encryptionKey === 'function') {
@@ -37,7 +38,7 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
3738
}
3839

3940
const middleware = createMiddleware(async (c, next) => {
40-
const session = new Session
41+
const session = new Session(expireAfterSeconds)
4142
let sid = ''
4243
let session_data: SessionData | null | undefined
4344
let createNewSession = false
@@ -65,7 +66,9 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
6566
session.setCache(session_data)
6667

6768
if (session.sessionValid()) {
68-
session.reupSession(expireAfterSeconds)
69+
if (autoExtendExpiration) {
70+
session.reupSession()
71+
}
6972
} else {
7073
store instanceof CookieStore ? await store.deleteSession(c) : await store.deleteSession(sid)
7174
createNewSession = true
@@ -93,19 +96,25 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
9396
await store.createSession(sid, defaultData)
9497
}
9598

96-
session.setCache(defaultData)
99+
session.setCache(defaultData, true)
97100
}
98101

99102
if (!(store instanceof CookieStore)) {
100103
setCookie(c, sessionCookieName, encryptionKey ? await encrypt(encryptionKey, sid) : sid, cookieOptions)
101104
}
102105

103-
session.updateAccess()
106+
if (autoExtendExpiration) {
107+
session.updateAccess()
108+
}
104109

105110
c.set('session', session)
106111

107112
await next()
108113

114+
if (session.isStale()) {
115+
session.touch()
116+
}
117+
109118
const shouldDelete = session.getCache()._delete;
110119
const shouldRotateSessionKey = c.get("session_key_rotation") === true;
111120
const storeIsCookieStore = store instanceof CookieStore;
@@ -146,7 +155,8 @@ export function sessionMiddleware(options: SessionOptions): MiddlewareHandler {
146155
*/
147156
const shouldPersistSession =
148157
!shouldDelete &&
149-
(!shouldRotateSessionKey || storeIsCookieStore);
158+
(!shouldRotateSessionKey || storeIsCookieStore) &&
159+
session.isStale();
150160

151161
if (shouldPersistSession) {
152162
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 | (() => string),
88
expireAfterSeconds?: number,
99
cookieOptions?: CookieOptions,
10-
sessionCookieName?: string
11-
}
10+
sessionCookieName?: string,
11+
autoExtendExpiration?: boolean
12+
}

0 commit comments

Comments
 (0)