Skip to content

Commit a168f99

Browse files
authored
Drop legacy client support. Switch to Typescript (#377)
This drops support for the following legacy clients: redis@v3 redis-mock This also rewrites the codebase in TypeScript removing the need to include a separate @types/connect-redis dependency. Build now supports both CJS and ESM. Support for Node 14 has been removed.
1 parent e5c6cd1 commit a168f99

15 files changed

+487
-431
lines changed

.eslintrc

+15-3
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,26 @@
11
{
22
"root": true,
3-
"extends": ["eslint:recommended"],
3+
"parser": "@typescript-eslint/parser",
4+
"plugins": ["@typescript-eslint"],
5+
"extends": [
6+
"eslint:recommended",
7+
"plugin:@typescript-eslint/recommended",
8+
"prettier"
9+
],
410
"env": {
511
"node": true,
612
"es6": true
713
},
814
"parserOptions": {
9-
"ecmaVersion": 8
15+
"sourceType": "module",
16+
"project": "./tsconfig.json",
17+
"ecmaVersion": 2020
1018
},
1119
"rules": {
12-
"no-console": 0
20+
"prefer-const": 0,
21+
"@typescript-eslint/no-explicit-any": 0,
22+
"@typescript-eslint/no-empty-function": 0,
23+
"@typescript-eslint/explicit-function-return-type": 0,
24+
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}]
1325
}
1426
}

.github/workflows/build.yml

+5-5
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
node: [14.x, 16.x, 18.x]
12+
node: [16, 18]
1313
name: Node v${{ matrix.node }}
1414
steps:
1515
- uses: actions/checkout@v3
1616
- uses: actions/setup-node@v3
1717
with:
1818
node-version: ${{ matrix.node }}
1919
- run: sudo apt-get install -y redis-server
20-
- run: yarn install
21-
- run: yarn fmt-check
22-
- run: yarn lint
23-
- run: yarn test
20+
- run: npm install
21+
- run: npm run fmt-check
22+
- run: npm run lint
23+
- run: npm test

.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,6 @@ yarn-error.log
55
package-lock.json
66
yarn.lock
77
.DS_Store
8+
dump.rdb
9+
pnpm-lock.yaml
10+
dist

.npmignore

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
testdata
2+
coverage
3+
.nyc_output
4+
5+
package-lock.json
6+
yarn.lock
7+
pnpm-lock.yaml
8+
9+
dump.rdb

.prettierrc

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{
2-
"tabWidth": 2,
3-
"semi": false
2+
"semi": false,
3+
"bracketSpacing": false,
4+
"plugins": ["prettier-plugin-organize-imports"]
45
}

index.js

-1
This file was deleted.

index.ts

+204
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
import {SessionData, Store} from "express-session"
2+
3+
const noop = (_err?: unknown, _data?: any) => {}
4+
5+
interface NormalizedRedisClient {
6+
get(key: string): Promise<string | null>
7+
set(key: string, value: string, ttl?: number): Promise<string | null>
8+
expire(key: string, ttl: number): Promise<number | boolean>
9+
scanIterator(match: string, count: number): AsyncIterable<string>
10+
del(key: string[]): Promise<number>
11+
mget(key: string[]): Promise<(string | null)[]>
12+
}
13+
14+
interface Serializer {
15+
parse(s: string): SessionData
16+
stringify(s: SessionData): string
17+
}
18+
19+
interface RedisStoreOptions {
20+
client: any
21+
prefix?: string
22+
scanCount?: number
23+
serializer?: Serializer
24+
ttl?: number
25+
disableTTL?: boolean
26+
disableTouch?: boolean
27+
}
28+
29+
class RedisStore extends Store {
30+
client: NormalizedRedisClient
31+
prefix: string
32+
scanCount: number
33+
serializer: Serializer
34+
ttl: number
35+
disableTTL: boolean
36+
disableTouch: boolean
37+
38+
constructor(opts: RedisStoreOptions) {
39+
super()
40+
this.prefix = opts.prefix == null ? "sess:" : opts.prefix
41+
this.scanCount = opts.scanCount || 100
42+
this.serializer = opts.serializer || JSON
43+
this.ttl = opts.ttl || 86400 // One day in seconds.
44+
this.disableTTL = opts.disableTTL || false
45+
this.disableTouch = opts.disableTouch || false
46+
this.client = this.normalizeClient(opts.client)
47+
}
48+
49+
// Create a redis and ioredis compatible client
50+
private normalizeClient(client: any): NormalizedRedisClient {
51+
let isRedis = "scanIterator" in client
52+
return {
53+
get: (key) => client.get(key),
54+
set: (key, val, ttl) => {
55+
if (ttl) {
56+
return isRedis
57+
? client.set(key, val, {EX: ttl})
58+
: client.set(key, val, "EX", ttl)
59+
}
60+
return client.set(key, val)
61+
},
62+
del: (key) => client.del(key),
63+
expire: (key, ttl) => client.expire(key, ttl),
64+
mget: (keys) => (isRedis ? client.mGet(keys) : client.mget(keys)),
65+
scanIterator: (match, count) => {
66+
if (isRedis) return client.scanIterator({MATCH: match, COUNT: count})
67+
68+
// ioredis impl.
69+
return (async function* () {
70+
let [c, xs] = await client.scan("0", "MATCH", match, "COUNT", count)
71+
for (let key of xs) yield key
72+
while (c !== "0") {
73+
;[c, xs] = await client.scan(c, "MATCH", match, "COUNT", count)
74+
for (let key of xs) yield key
75+
}
76+
})()
77+
},
78+
}
79+
}
80+
81+
async get(sid: string, cb = noop) {
82+
let key = this.prefix + sid
83+
try {
84+
let data = await this.client.get(key)
85+
if (!data) return cb()
86+
return cb(null, this.serializer.parse(data))
87+
} catch (err) {
88+
return cb(err)
89+
}
90+
}
91+
92+
async set(sid: string, sess: SessionData, cb = noop) {
93+
let key = this.prefix + sid
94+
let ttl = this._getTTL(sess)
95+
try {
96+
let val = this.serializer.stringify(sess)
97+
if (ttl > 0) {
98+
if (this.disableTTL) await this.client.set(key, val)
99+
else await this.client.set(key, val, ttl)
100+
return cb()
101+
} else {
102+
return this.destroy(sid, cb)
103+
}
104+
} catch (err) {
105+
return cb(err)
106+
}
107+
}
108+
109+
async touch(sid: string, sess: SessionData, cb = noop) {
110+
let key = this.prefix + sid
111+
if (this.disableTouch || this.disableTTL) return cb()
112+
try {
113+
await this.client.expire(key, this._getTTL(sess))
114+
return cb()
115+
} catch (err) {
116+
return cb(err)
117+
}
118+
}
119+
120+
async destroy(sid: string, cb = noop) {
121+
let key = this.prefix + sid
122+
try {
123+
await this.client.del([key])
124+
return cb()
125+
} catch (err) {
126+
return cb(err)
127+
}
128+
}
129+
130+
async clear(cb = noop) {
131+
try {
132+
let keys = await this._getAllKeys()
133+
if (!keys.length) return cb()
134+
await this.client.del(keys)
135+
return cb()
136+
} catch (err) {
137+
return cb(err)
138+
}
139+
}
140+
141+
async length(cb = noop) {
142+
try {
143+
let keys = await this._getAllKeys()
144+
return cb(null, keys.length)
145+
} catch (err) {
146+
return cb(err)
147+
}
148+
}
149+
150+
async ids(cb = noop) {
151+
let len = this.prefix.length
152+
try {
153+
let keys = await this._getAllKeys()
154+
return cb(
155+
null,
156+
keys.map((k) => k.substring(len))
157+
)
158+
} catch (err) {
159+
return cb(err)
160+
}
161+
}
162+
163+
async all(cb = noop) {
164+
let len = this.prefix.length
165+
try {
166+
let keys = await this._getAllKeys()
167+
if (keys.length === 0) return cb(null, [])
168+
169+
let data = await this.client.mget(keys)
170+
let results = data.reduce((acc, raw, idx) => {
171+
if (!raw) return acc
172+
let sess = this.serializer.parse(raw) as any
173+
sess.id = keys[idx].substring(len)
174+
acc.push(sess)
175+
return acc
176+
}, [] as SessionData[])
177+
return cb(null, results)
178+
} catch (err) {
179+
return cb(err)
180+
}
181+
}
182+
183+
private _getTTL(sess: SessionData) {
184+
let ttl
185+
if (sess && sess.cookie && sess.cookie.expires) {
186+
let ms = Number(new Date(sess.cookie.expires)) - Date.now()
187+
ttl = Math.ceil(ms / 1000)
188+
} else {
189+
ttl = this.ttl
190+
}
191+
return ttl
192+
}
193+
194+
private async _getAllKeys() {
195+
let pattern = this.prefix + "*"
196+
let keys = []
197+
for await (let key of this.client.scanIterator(pattern, this.scanCount)) {
198+
keys.push(key)
199+
}
200+
return keys
201+
}
202+
}
203+
204+
export default RedisStore

0 commit comments

Comments
 (0)