Skip to content

Commit bfb6b35

Browse files
authored
Fix merging bug in 6.1.0 that resulted in sessions being deleted. (#348)
`deepmerge` is fairly battle tested and handles the many edge cases when it comes to merging objects deeply in JS. #346
1 parent d46afe4 commit bfb6b35

File tree

3 files changed

+86
-30
lines changed

3 files changed

+86
-30
lines changed

lib/connect-redis.js

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,15 @@
44
* MIT Licensed
55
*/
66

7+
const deepmerge = require("deepmerge")
8+
79
module.exports = function (session) {
810
const Store = session.Store
911

1012
// All callbacks should have a noop if none provided for compatibility
1113
// with the most Redis clients.
1214
const noop = () => {}
15+
const TOMBSTONE = "TOMBSTONE"
1316

1417
class RedisStore extends Store {
1518
constructor(options = {}) {
@@ -27,12 +30,13 @@ module.exports = function (session) {
2730
this.disableTouch = options.disableTouch || false
2831
}
2932

30-
get(sid, cb = noop) {
33+
get(sid, cb = noop, showTombs = false) {
3134
let key = this.prefix + sid
3235

3336
this.client.get(key, (err, data) => {
3437
if (err) return cb(err)
3538
if (!data) return cb()
39+
if (data === TOMBSTONE) return cb(null, showTombs ? data : undefined)
3640

3741
let result
3842
try {
@@ -45,28 +49,40 @@ module.exports = function (session) {
4549
}
4650

4751
set(sid, sess, cb = noop) {
48-
let args = [this.prefix + sid]
49-
50-
let value
51-
try {
52-
value = this.serializer.stringify(sess)
53-
} catch (er) {
54-
return cb(er)
55-
}
56-
args.push(value)
52+
this.get(
53+
sid,
54+
(err, oldSess) => {
55+
if (oldSess === TOMBSTONE) {
56+
return cb()
57+
} else if (oldSess && oldSess.lastModified !== sess.lastModified) {
58+
sess = deepmerge(oldSess, sess)
59+
}
60+
let args = [this.prefix + sid]
61+
let value
62+
sess.lastModified = Date.now()
63+
try {
64+
value = this.serializer.stringify(sess)
65+
} catch (er) {
66+
return cb(er)
67+
}
68+
args.push(value)
69+
args.push("EX", this._getTTL(sess))
5770

58-
let ttl = 1
59-
if (!this.disableTTL) {
60-
ttl = this._getTTL(sess)
61-
args.push("EX", ttl)
62-
}
71+
let ttl = 1
72+
if (!this.disableTTL) {
73+
ttl = this._getTTL(sess)
74+
args.push("EX", ttl)
75+
}
6376

64-
if (ttl > 0) {
65-
this.client.set(args, cb)
66-
} else {
67-
// If the resulting TTL is negative we can delete / destroy the key
68-
this.destroy(sid, cb)
69-
}
77+
if (ttl > 0) {
78+
this.client.set(args, cb)
79+
} else {
80+
// If the resulting TTL is negative we can delete / destroy the key
81+
this.destroy(sid, cb)
82+
}
83+
},
84+
true
85+
)
7086
}
7187

7288
touch(sid, sess, cb = noop) {
@@ -81,7 +97,9 @@ module.exports = function (session) {
8197

8298
destroy(sid, cb = noop) {
8399
let key = this.prefix + sid
84-
this.client.del(key, cb)
100+
this.client.set([key, TOMBSTONE, "EX", 300], (err) => {
101+
cb(err, 1)
102+
})
85103
}
86104

87105
clear(cb = noop) {
@@ -92,9 +110,9 @@ module.exports = function (session) {
92110
}
93111

94112
length(cb = noop) {
95-
this._getAllKeys((err, keys) => {
113+
this.all((err, result) => {
96114
if (err) return cb(err)
97-
return cb(null, keys.length)
115+
return cb(null, result.length)
98116
})
99117
}
100118

@@ -121,7 +139,7 @@ module.exports = function (session) {
121139
let result
122140
try {
123141
result = sessions.reduce((accum, data, index) => {
124-
if (!data) return accum
142+
if (!data || data === TOMBSTONE) return accum
125143
data = this.serializer.parse(data)
126144
data.id = keys[index].substr(prefixLen)
127145
accum.push(data)

package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
"eslint-config-prettier": "^8.3.0",
1919
"express-session": "^1.17.0",
2020
"ioredis": "^4.17.1",
21+
"mockdate": "^3.0.5",
2122
"nyc": "^15.0.1",
2223
"prettier": "^2.0.5",
2324
"redis-mock": "^0.56.3",
@@ -36,6 +37,9 @@
3637
"fmt": "prettier --write .",
3738
"fmt-check": "prettier --check ."
3839
},
40+
"dependencies": {
41+
"deepmerge": "^4.2.2"
42+
},
3943
"keywords": [
4044
"connect",
4145
"redis",

test/connect-redis-test.js

Lines changed: 39 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@ const redisV3 = require("redis-v3")
55
const redisV4 = require("redis-v4")
66
const ioRedis = require("ioredis")
77
const redisMock = require("redis-mock")
8+
const MockDate = require("mockdate")
89

910
let RedisStore = require("../")(session)
11+
MockDate.set("2000-11-22")
1012

1113
let p =
1214
(ctx, method) =>
@@ -70,11 +72,34 @@ test("redis-mock client", async (t) => {
7072
test("teardown", redisSrv.disconnect)
7173

7274
async function lifecycleTest(store, t) {
73-
let res = await p(store, "set")("123", { foo: "bar" })
75+
await p(store, "set")("123", { foo: "bar3" })
76+
let res = await p(store, "get")("123")
77+
t.same(res, { foo: "bar3", lastModified: 974851200000 }, "get value 1")
78+
await p(store, "set")("123", {
79+
foo: "bar3",
80+
luke: "skywalker",
81+
obi: "wan",
82+
lastModified: 974851000000,
83+
})
84+
await p(store, "set")("123", {
85+
luke: "skywalker",
86+
lastModified: 974851000000,
87+
})
88+
res = await p(store, "get")("123")
89+
t.same(
90+
res,
91+
{ foo: "bar3", luke: "skywalker", obi: "wan", lastModified: 974851200000 },
92+
"get merged value"
93+
)
94+
95+
res = await p(store, "clear")()
96+
t.ok(res >= 1, "cleared key")
97+
98+
res = await p(store, "set")("123", { foo: "bar" })
7499
t.equal(res, "OK", "set value")
75100

76101
res = await p(store, "get")("123")
77-
t.same(res, { foo: "bar" }, "get value")
102+
t.same(res, { foo: "bar", lastModified: 974851200000 }, "get value")
78103

79104
res = await p(store.client, "ttl")("sess:123")
80105
t.ok(res >= 86399, "check one day ttl")
@@ -108,20 +133,29 @@ async function lifecycleTest(store, t) {
108133
t.same(
109134
res,
110135
[
111-
{ id: "123", foo: "bar" },
112-
{ id: "456", cookie: { expires } },
136+
{ id: "123", foo: "bar", lastModified: 974851200000 },
137+
{ id: "456", cookie: { expires }, lastModified: 974851200000 },
113138
],
114139
"stored two keys data"
115140
)
116141

117142
res = await p(store, "destroy")("456")
118143
t.equal(res, 1, "destroyed one")
119144

145+
res = await p(store, "get")("456")
146+
t.equal(res, undefined, "tombstoned one")
147+
148+
res = await p(store, "set")("456", { a: "new hope" })
149+
t.equal(res, undefined, "tombstoned set")
150+
151+
res = await p(store, "get")("456")
152+
t.equal(res, undefined, "tombstoned two")
153+
120154
res = await p(store, "length")()
121155
t.equal(res, 1, "one key remains")
122156

123157
res = await p(store, "clear")()
124-
t.equal(res, 1, "cleared remaining key")
158+
t.equal(res, 2, "cleared remaining key")
125159

126160
res = await p(store, "length")()
127161
t.equal(res, 0, "no key remains")

0 commit comments

Comments
 (0)