Skip to content

Commit e249571

Browse files
authored
Fix broken windows (#431)
Includes: - BREAKING: Drop support for `ioredis` - BREAKING: Support `redis` v5 and later - Switch to Biome from Prettier/ESLint - Update `readme.md` installation instructions Ref: https://github.com/redis/node-redis Ref: https://redis.io/docs/latest/develop/clients/nodejs/migration/
1 parent 536b465 commit e249571

File tree

9 files changed

+108
-182
lines changed

9 files changed

+108
-182
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ jobs:
99
runs-on: ubuntu-latest
1010
strategy:
1111
matrix:
12-
node: [18, 20, 22]
12+
node: [18, 20, 22, 24]
1313
name: Node v${{ matrix.node }}
1414
steps:
1515
- uses: actions/checkout@v3
@@ -18,6 +18,5 @@ jobs:
1818
node-version: ${{ matrix.node }}
1919
- run: sudo apt-get install -y redis-server
2020
- run: npm install
21-
- run: npm run fmt-check
2221
- run: npm run lint
2322
- run: npm test

.prettierrc

Lines changed: 0 additions & 5 deletions
This file was deleted.

biome.json

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
{
2+
"files": {
3+
"ignore": ["dist", "pnpm-*.yaml", "coverage"]
4+
},
5+
"formatter": {
6+
"enabled": true,
7+
"indentStyle": "space",
8+
"bracketSpacing": false
9+
},
10+
"linter": {
11+
"enabled": true,
12+
"rules": {
13+
"a11y": {
14+
"all": false
15+
},
16+
"style": {
17+
"useConst": "off",
18+
"useTemplate": "off",
19+
"noParameterAssign": "off",
20+
"useSingleVarDeclarator": "off"
21+
},
22+
"correctness": {
23+
"noUnusedImports": "error"
24+
},
25+
"suspicious": {
26+
"noExplicitAny": "off",
27+
"noArrayIndexKey": "off",
28+
"noImplicitAnyLet": "off"
29+
}
30+
}
31+
},
32+
"javascript": {
33+
"formatter": {
34+
"semicolons": "asNeeded"
35+
}
36+
},
37+
"organizeImports": {
38+
"enabled": true
39+
}
40+
}

eslint.config.js

Lines changed: 0 additions & 23 deletions
This file was deleted.

index.ts

Lines changed: 47 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import {SessionData, Store} from "express-session"
1+
import {type SessionData, Store} from "express-session"
2+
import type {RedisClientType, RedisClusterType} from "redis"
23

34
type Callback = (_err?: unknown, _data?: any) => any
45

@@ -8,15 +9,6 @@ function optionalCb(err: unknown, data: unknown, cb?: Callback) {
89
return data
910
}
1011

11-
interface NormalizedRedisClient {
12-
get(key: string): Promise<string | null>
13-
set(key: string, value: string, ttl?: number): Promise<string | null>
14-
expire(key: string, ttl: number): Promise<number | boolean>
15-
scanIterator(match: string, count: number): AsyncIterable<string>
16-
del(key: string[]): Promise<number>
17-
mget(key: string[]): Promise<(string | null)[]>
18-
}
19-
2012
interface Serializer {
2113
parse(s: string): SessionData | Promise<SessionData>
2214
stringify(s: SessionData): string
@@ -27,17 +19,17 @@ interface RedisStoreOptions {
2719
prefix?: string
2820
scanCount?: number
2921
serializer?: Serializer
30-
ttl?: number | {(sess: SessionData): number}
22+
ttl?: number | ((sess: SessionData) => number)
3123
disableTTL?: boolean
3224
disableTouch?: boolean
3325
}
3426

3527
export class RedisStore extends Store {
36-
client: NormalizedRedisClient
28+
client: RedisClientType | RedisClusterType
3729
prefix: string
3830
scanCount: number
3931
serializer: Serializer
40-
ttl: number | {(sess: SessionData): number}
32+
ttl: number | ((sess: SessionData) => number)
4133
disableTTL: boolean
4234
disableTouch: boolean
4335

@@ -49,57 +41,7 @@ export class RedisStore extends Store {
4941
this.ttl = opts.ttl || 86400 // One day in seconds.
5042
this.disableTTL = opts.disableTTL || false
5143
this.disableTouch = opts.disableTouch || false
52-
this.client = this.normalizeClient(opts.client)
53-
}
54-
55-
// Create a redis and ioredis compatible client
56-
private normalizeClient(client: any): NormalizedRedisClient {
57-
let isRedis = "scanIterator" in client || "masters" in client
58-
let isRedisCluster = "masters" in client
59-
60-
return {
61-
get: (key) => client.get(key),
62-
set: (key, val, ttl) => {
63-
if (ttl) {
64-
return isRedis
65-
? client.set(key, val, {EX: ttl})
66-
: client.set(key, val, "EX", ttl)
67-
}
68-
return client.set(key, val)
69-
},
70-
del: (key) => client.del(key),
71-
expire: (key, ttl) => client.expire(key, ttl),
72-
mget: (keys) => (isRedis ? client.mGet(keys) : client.mget(keys)),
73-
scanIterator: (match, count) => {
74-
// node-redis createCluster impl.
75-
if (isRedisCluster) {
76-
return (async function* () {
77-
for (const master of client.masters) {
78-
const nodeClient = await client.nodeClient(master)
79-
80-
for await (const key of nodeClient.scanIterator({
81-
COUNT: count,
82-
MATCH: match,
83-
})) {
84-
yield key
85-
}
86-
}
87-
})()
88-
}
89-
90-
if (isRedis) return client.scanIterator({MATCH: match, COUNT: count})
91-
92-
// ioredis impl.
93-
return (async function* () {
94-
let [c, xs] = await client.scan("0", "MATCH", match, "COUNT", count)
95-
for (let key of xs) yield key
96-
while (c !== "0") {
97-
;[c, xs] = await client.scan(c, "MATCH", match, "COUNT", count)
98-
for (let key of xs) yield key
99-
}
100-
})()
101-
},
102-
}
44+
this.client = opts.client
10345
}
10446

10547
async get(sid: string, cb?: Callback) {
@@ -115,16 +57,18 @@ export class RedisStore extends Store {
11557

11658
async set(sid: string, sess: SessionData, cb?: Callback) {
11759
let key = this.prefix + sid
118-
let ttl = this._getTTL(sess)
60+
let ttl = this.getTTL(sess)
11961
try {
12062
if (ttl > 0) {
12163
let val = this.serializer.stringify(sess)
12264
if (this.disableTTL) await this.client.set(key, val)
123-
else await this.client.set(key, val, ttl)
65+
else
66+
await this.client.set(key, val, {
67+
expiration: {type: "EX", value: ttl},
68+
})
12469
return optionalCb(null, null, cb)
125-
} else {
126-
return this.destroy(sid, cb)
12770
}
71+
return this.destroy(sid, cb)
12872
} catch (err) {
12973
return optionalCb(err, null, cb)
13074
}
@@ -134,7 +78,7 @@ export class RedisStore extends Store {
13478
let key = this.prefix + sid
13579
if (this.disableTouch || this.disableTTL) return optionalCb(null, null, cb)
13680
try {
137-
await this.client.expire(key, this._getTTL(sess))
81+
await this.client.expire(key, this.getTTL(sess))
13882
return optionalCb(null, null, cb)
13983
} catch (err) {
14084
return optionalCb(err, null, cb)
@@ -153,7 +97,7 @@ export class RedisStore extends Store {
15397

15498
async clear(cb?: Callback) {
15599
try {
156-
let keys = await this._getAllKeys()
100+
let keys = await this.getAllKeys()
157101
if (!keys.length) return optionalCb(null, null, cb)
158102
await this.client.del(keys)
159103
return optionalCb(null, null, cb)
@@ -164,7 +108,7 @@ export class RedisStore extends Store {
164108

165109
async length(cb?: Callback) {
166110
try {
167-
let keys = await this._getAllKeys()
111+
let keys = await this.getAllKeys()
168112
return optionalCb(null, keys.length, cb)
169113
} catch (err) {
170114
return optionalCb(err, null, cb)
@@ -174,7 +118,7 @@ export class RedisStore extends Store {
174118
async ids(cb?: Callback) {
175119
let len = this.prefix.length
176120
try {
177-
let keys = await this._getAllKeys()
121+
let keys = await this.getAllKeys()
178122
return optionalCb(
179123
null,
180124
keys.map((k) => k.substring(len)),
@@ -188,10 +132,10 @@ export class RedisStore extends Store {
188132
async all(cb?: Callback) {
189133
let len = this.prefix.length
190134
try {
191-
let keys = await this._getAllKeys()
135+
let keys = await this.getAllKeys()
192136
if (keys.length === 0) return optionalCb(null, [], cb)
193137

194-
let data = await this.client.mget(keys)
138+
let data = await this.client.mGet(keys)
195139
let results = data.reduce((acc, raw, idx) => {
196140
if (!raw) return acc
197141
let sess = this.serializer.parse(raw) as any
@@ -205,13 +149,13 @@ export class RedisStore extends Store {
205149
}
206150
}
207151

208-
private _getTTL(sess: SessionData) {
152+
private getTTL(sess: SessionData) {
209153
if (typeof this.ttl === "function") {
210154
return this.ttl(sess)
211155
}
212156

213157
let ttl
214-
if (sess && sess.cookie && sess.cookie.expires) {
158+
if (sess?.cookie?.expires) {
215159
let ms = Number(new Date(sess.cookie.expires)) - Date.now()
216160
ttl = Math.ceil(ms / 1000)
217161
} else {
@@ -220,12 +164,34 @@ export class RedisStore extends Store {
220164
return ttl
221165
}
222166

223-
private async _getAllKeys() {
167+
private async getAllKeys() {
224168
let pattern = this.prefix + "*"
225-
let keys = []
226-
for await (let key of this.client.scanIterator(pattern, this.scanCount)) {
227-
keys.push(key)
169+
let set = new Set<string>()
170+
for await (let keys of this.scanIterator(pattern, this.scanCount)) {
171+
for (let key of keys) {
172+
set.add(key)
173+
}
174+
}
175+
return set.size > 0 ? Array.from(set) : []
176+
}
177+
178+
private scanIterator(match: string, count: number) {
179+
let client = this.client
180+
181+
if (!("masters" in client)) {
182+
return client.scanIterator({MATCH: match, COUNT: count})
228183
}
229-
return keys
184+
185+
return (async function* () {
186+
for (let master of client.masters) {
187+
let c = await client.nodeClient(master)
188+
for await (let keys of c.scanIterator({
189+
COUNT: count,
190+
MATCH: match,
191+
})) {
192+
yield keys
193+
}
194+
}
195+
})()
230196
}
231197
}

index_test.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {Cookie} from "express-session"
2-
import {Redis} from "ioredis"
32
import {createClient} from "redis"
43
import {expect, test} from "vitest"
54
import {RedisStore} from "./"
@@ -22,22 +21,15 @@ test("defaults", async () => {
2221
expect(store.serializer).toBe(JSON)
2322
expect(store.disableTouch).toBe(false)
2423
expect(store.disableTTL).toBe(false)
25-
await client.disconnect()
24+
client.destroy()
2625
})
2726

2827
test("redis", async () => {
2928
let client = createClient({url: `redis://localhost:${redisSrv.port}`})
3029
await client.connect()
3130
let store = new RedisStore({client})
3231
await lifecycleTest(store, client)
33-
await client.disconnect()
34-
})
35-
36-
test("ioredis", async () => {
37-
let client = new Redis(`redis://localhost:${redisSrv.port}`)
38-
let store = new RedisStore({client})
39-
await lifecycleTest(store, client)
40-
client.disconnect()
32+
client.destroy()
4133
})
4234

4335
test("teardown", redisSrv.disconnect)

0 commit comments

Comments
 (0)