connect-redis
Advanced tools
Comparing version
@@ -0,1 +1,3 @@ | ||
import { RedisClientType } from 'redis'; | ||
import { RedisClusterType } from 'redis'; | ||
import { SessionData } from 'express-session'; | ||
@@ -6,23 +8,11 @@ import { Store } from 'express-session'; | ||
declare interface NormalizedRedisClient { | ||
get(key: string): Promise<string | null>; | ||
set(key: string, value: string, ttl?: number): Promise<string | null>; | ||
expire(key: string, ttl: number): Promise<number | boolean>; | ||
scanIterator(match: string, count: number): AsyncIterable<string>; | ||
del(key: string[]): Promise<number>; | ||
mget(key: string[]): Promise<(string | null)[]>; | ||
} | ||
export declare class RedisStore extends Store { | ||
client: NormalizedRedisClient; | ||
client: RedisClientType | RedisClusterType; | ||
prefix: string; | ||
scanCount: number; | ||
serializer: Serializer; | ||
ttl: number | { | ||
(sess: SessionData): number; | ||
}; | ||
ttl: number | ((sess: SessionData) => number); | ||
disableTTL: boolean; | ||
disableTouch: boolean; | ||
constructor(opts: RedisStoreOptions); | ||
private normalizeClient; | ||
get(sid: string, cb?: Callback): Promise<any>; | ||
@@ -36,4 +26,5 @@ set(sid: string, sess: SessionData, cb?: Callback): Promise<any>; | ||
all(cb?: Callback): Promise<any>; | ||
private _getTTL; | ||
private _getAllKeys; | ||
private getTTL; | ||
private getAllKeys; | ||
private scanIterator; | ||
} | ||
@@ -46,5 +37,3 @@ | ||
serializer?: Serializer; | ||
ttl?: number | { | ||
(sess: SessionData): number; | ||
}; | ||
ttl?: number | ((sess: SessionData) => number); | ||
disableTTL?: boolean; | ||
@@ -51,0 +40,0 @@ disableTouch?: boolean; |
@@ -23,46 +23,4 @@ import { Store } from "express-session"; | ||
this.disableTouch = opts.disableTouch || false; | ||
this.client = this.normalizeClient(opts.client); | ||
this.client = opts.client; | ||
} | ||
// Create a redis and ioredis compatible client | ||
normalizeClient(client) { | ||
let isRedis = "scanIterator" in client || "masters" in client; | ||
let isRedisCluster = "masters" in client; | ||
return { | ||
get: (key) => client.get(key), | ||
set: (key, val, ttl) => { | ||
if (ttl) { | ||
return isRedis ? client.set(key, val, { EX: ttl }) : client.set(key, val, "EX", ttl); | ||
} | ||
return client.set(key, val); | ||
}, | ||
del: (key) => client.del(key), | ||
expire: (key, ttl) => client.expire(key, ttl), | ||
mget: (keys) => isRedis ? client.mGet(keys) : client.mget(keys), | ||
scanIterator: (match, count) => { | ||
if (isRedisCluster) { | ||
return async function* () { | ||
for (const master of client.masters) { | ||
const nodeClient = await client.nodeClient(master); | ||
for await (const key of nodeClient.scanIterator({ | ||
COUNT: count, | ||
MATCH: match | ||
})) { | ||
yield key; | ||
} | ||
} | ||
}(); | ||
} | ||
if (isRedis) return client.scanIterator({ MATCH: match, COUNT: count }); | ||
return async function* () { | ||
let [c, xs] = await client.scan("0", "MATCH", match, "COUNT", count); | ||
for (let key of xs) yield key; | ||
while (c !== "0") { | ||
; | ||
[c, xs] = await client.scan(c, "MATCH", match, "COUNT", count); | ||
for (let key of xs) yield key; | ||
} | ||
}(); | ||
} | ||
}; | ||
} | ||
async get(sid, cb) { | ||
@@ -80,3 +38,3 @@ let key = this.prefix + sid; | ||
let key = this.prefix + sid; | ||
let ttl = this._getTTL(sess); | ||
let ttl = this.getTTL(sess); | ||
try { | ||
@@ -86,7 +44,9 @@ if (ttl > 0) { | ||
if (this.disableTTL) await this.client.set(key, val); | ||
else await this.client.set(key, val, ttl); | ||
else | ||
await this.client.set(key, val, { | ||
expiration: { type: "EX", value: ttl } | ||
}); | ||
return optionalCb(null, null, cb); | ||
} else { | ||
return this.destroy(sid, cb); | ||
} | ||
return this.destroy(sid, cb); | ||
} catch (err) { | ||
@@ -100,3 +60,3 @@ return optionalCb(err, null, cb); | ||
try { | ||
await this.client.expire(key, this._getTTL(sess)); | ||
await this.client.expire(key, this.getTTL(sess)); | ||
return optionalCb(null, null, cb); | ||
@@ -118,3 +78,3 @@ } catch (err) { | ||
try { | ||
let keys = await this._getAllKeys(); | ||
let keys = await this.getAllKeys(); | ||
if (!keys.length) return optionalCb(null, null, cb); | ||
@@ -129,3 +89,3 @@ await this.client.del(keys); | ||
try { | ||
let keys = await this._getAllKeys(); | ||
let keys = await this.getAllKeys(); | ||
return optionalCb(null, keys.length, cb); | ||
@@ -139,3 +99,3 @@ } catch (err) { | ||
try { | ||
let keys = await this._getAllKeys(); | ||
let keys = await this.getAllKeys(); | ||
return optionalCb( | ||
@@ -153,5 +113,5 @@ null, | ||
try { | ||
let keys = await this._getAllKeys(); | ||
let keys = await this.getAllKeys(); | ||
if (keys.length === 0) return optionalCb(null, [], cb); | ||
let data = await this.client.mget(keys); | ||
let data = await this.client.mGet(keys); | ||
let results = data.reduce((acc, raw, idx) => { | ||
@@ -169,3 +129,3 @@ if (!raw) return acc; | ||
} | ||
_getTTL(sess) { | ||
getTTL(sess) { | ||
if (typeof this.ttl === "function") { | ||
@@ -175,3 +135,3 @@ return this.ttl(sess); | ||
let ttl; | ||
if (sess && sess.cookie && sess.cookie.expires) { | ||
if (sess?.cookie?.expires) { | ||
let ms = Number(new Date(sess.cookie.expires)) - Date.now(); | ||
@@ -184,10 +144,29 @@ ttl = Math.ceil(ms / 1e3); | ||
} | ||
async _getAllKeys() { | ||
async getAllKeys() { | ||
let pattern = this.prefix + "*"; | ||
let keys = []; | ||
for await (let key of this.client.scanIterator(pattern, this.scanCount)) { | ||
keys.push(key); | ||
let set = /* @__PURE__ */ new Set(); | ||
for await (let keys of this.scanIterator(pattern, this.scanCount)) { | ||
for (let key of keys) { | ||
set.add(key); | ||
} | ||
} | ||
return keys; | ||
return set.size > 0 ? Array.from(set) : []; | ||
} | ||
scanIterator(match, count) { | ||
let client = this.client; | ||
if (!("masters" in client)) { | ||
return client.scanIterator({ MATCH: match, COUNT: count }); | ||
} | ||
return async function* () { | ||
for (let master of client.masters) { | ||
let c = await client.nodeClient(master); | ||
for await (let keys of c.scanIterator({ | ||
COUNT: count, | ||
MATCH: match | ||
})) { | ||
yield keys; | ||
} | ||
} | ||
}(); | ||
} | ||
} | ||
@@ -194,0 +173,0 @@ export { |
import {Cookie} from "express-session" | ||
import {Redis} from "ioredis" | ||
import {createClient} from "redis" | ||
@@ -25,3 +24,3 @@ import {expect, test} from "vitest" | ||
expect(store.disableTTL).toBe(false) | ||
await client.disconnect() | ||
client.destroy() | ||
}) | ||
@@ -34,12 +33,5 @@ | ||
await lifecycleTest(store, client) | ||
await client.disconnect() | ||
client.destroy() | ||
}) | ||
test("ioredis", async () => { | ||
let client = new Redis(`redis://localhost:${redisSrv.port}`) | ||
let store = new RedisStore({client}) | ||
await lifecycleTest(store, client) | ||
client.disconnect() | ||
}) | ||
test("teardown", redisSrv.disconnect) | ||
@@ -46,0 +38,0 @@ |
128
index.ts
@@ -1,2 +0,3 @@ | ||
import {SessionData, Store} from "express-session" | ||
import {type SessionData, Store} from "express-session" | ||
import type {RedisClientType, RedisClusterType} from "redis" | ||
@@ -11,11 +12,2 @@ type Callback = (_err?: unknown, _data?: any) => any | ||
interface NormalizedRedisClient { | ||
get(key: string): Promise<string | null> | ||
set(key: string, value: string, ttl?: number): Promise<string | null> | ||
expire(key: string, ttl: number): Promise<number | boolean> | ||
scanIterator(match: string, count: number): AsyncIterable<string> | ||
del(key: string[]): Promise<number> | ||
mget(key: string[]): Promise<(string | null)[]> | ||
} | ||
interface Serializer { | ||
@@ -31,3 +23,3 @@ parse(s: string): SessionData | Promise<SessionData> | ||
serializer?: Serializer | ||
ttl?: number | {(sess: SessionData): number} | ||
ttl?: number | ((sess: SessionData) => number) | ||
disableTTL?: boolean | ||
@@ -38,7 +30,7 @@ disableTouch?: boolean | ||
export class RedisStore extends Store { | ||
client: NormalizedRedisClient | ||
client: RedisClientType | RedisClusterType | ||
prefix: string | ||
scanCount: number | ||
serializer: Serializer | ||
ttl: number | {(sess: SessionData): number} | ||
ttl: number | ((sess: SessionData) => number) | ||
disableTTL: boolean | ||
@@ -55,55 +47,5 @@ disableTouch: boolean | ||
this.disableTouch = opts.disableTouch || false | ||
this.client = this.normalizeClient(opts.client) | ||
this.client = opts.client | ||
} | ||
// Create a redis and ioredis compatible client | ||
private normalizeClient(client: any): NormalizedRedisClient { | ||
let isRedis = "scanIterator" in client || "masters" in client | ||
let isRedisCluster = "masters" in client | ||
return { | ||
get: (key) => client.get(key), | ||
set: (key, val, ttl) => { | ||
if (ttl) { | ||
return isRedis | ||
? client.set(key, val, {EX: ttl}) | ||
: client.set(key, val, "EX", ttl) | ||
} | ||
return client.set(key, val) | ||
}, | ||
del: (key) => client.del(key), | ||
expire: (key, ttl) => client.expire(key, ttl), | ||
mget: (keys) => (isRedis ? client.mGet(keys) : client.mget(keys)), | ||
scanIterator: (match, count) => { | ||
// node-redis createCluster impl. | ||
if (isRedisCluster) { | ||
return (async function* () { | ||
for (const master of client.masters) { | ||
const nodeClient = await client.nodeClient(master) | ||
for await (const key of nodeClient.scanIterator({ | ||
COUNT: count, | ||
MATCH: match, | ||
})) { | ||
yield key | ||
} | ||
} | ||
})() | ||
} | ||
if (isRedis) return client.scanIterator({MATCH: match, COUNT: count}) | ||
// ioredis impl. | ||
return (async function* () { | ||
let [c, xs] = await client.scan("0", "MATCH", match, "COUNT", count) | ||
for (let key of xs) yield key | ||
while (c !== "0") { | ||
;[c, xs] = await client.scan(c, "MATCH", match, "COUNT", count) | ||
for (let key of xs) yield key | ||
} | ||
})() | ||
}, | ||
} | ||
} | ||
async get(sid: string, cb?: Callback) { | ||
@@ -122,3 +64,3 @@ let key = this.prefix + sid | ||
let key = this.prefix + sid | ||
let ttl = this._getTTL(sess) | ||
let ttl = this.getTTL(sess) | ||
try { | ||
@@ -128,7 +70,9 @@ if (ttl > 0) { | ||
if (this.disableTTL) await this.client.set(key, val) | ||
else await this.client.set(key, val, ttl) | ||
else | ||
await this.client.set(key, val, { | ||
expiration: {type: "EX", value: ttl}, | ||
}) | ||
return optionalCb(null, null, cb) | ||
} else { | ||
return this.destroy(sid, cb) | ||
} | ||
return this.destroy(sid, cb) | ||
} catch (err) { | ||
@@ -143,3 +87,3 @@ return optionalCb(err, null, cb) | ||
try { | ||
await this.client.expire(key, this._getTTL(sess)) | ||
await this.client.expire(key, this.getTTL(sess)) | ||
return optionalCb(null, null, cb) | ||
@@ -163,3 +107,3 @@ } catch (err) { | ||
try { | ||
let keys = await this._getAllKeys() | ||
let keys = await this.getAllKeys() | ||
if (!keys.length) return optionalCb(null, null, cb) | ||
@@ -175,3 +119,3 @@ await this.client.del(keys) | ||
try { | ||
let keys = await this._getAllKeys() | ||
let keys = await this.getAllKeys() | ||
return optionalCb(null, keys.length, cb) | ||
@@ -186,3 +130,3 @@ } catch (err) { | ||
try { | ||
let keys = await this._getAllKeys() | ||
let keys = await this.getAllKeys() | ||
return optionalCb( | ||
@@ -201,6 +145,6 @@ null, | ||
try { | ||
let keys = await this._getAllKeys() | ||
let keys = await this.getAllKeys() | ||
if (keys.length === 0) return optionalCb(null, [], cb) | ||
let data = await this.client.mget(keys) | ||
let data = await this.client.mGet(keys) | ||
let results = data.reduce((acc, raw, idx) => { | ||
@@ -219,3 +163,3 @@ if (!raw) return acc | ||
private _getTTL(sess: SessionData) { | ||
private getTTL(sess: SessionData) { | ||
if (typeof this.ttl === "function") { | ||
@@ -226,3 +170,3 @@ return this.ttl(sess) | ||
let ttl | ||
if (sess && sess.cookie && sess.cookie.expires) { | ||
if (sess?.cookie?.expires) { | ||
let ms = Number(new Date(sess.cookie.expires)) - Date.now() | ||
@@ -236,10 +180,32 @@ ttl = Math.ceil(ms / 1000) | ||
private async _getAllKeys() { | ||
private async getAllKeys() { | ||
let pattern = this.prefix + "*" | ||
let keys = [] | ||
for await (let key of this.client.scanIterator(pattern, this.scanCount)) { | ||
keys.push(key) | ||
let set = new Set<string>() | ||
for await (let keys of this.scanIterator(pattern, this.scanCount)) { | ||
for (let key of keys) { | ||
set.add(key) | ||
} | ||
} | ||
return keys | ||
return set.size > 0 ? Array.from(set) : [] | ||
} | ||
private scanIterator(match: string, count: number) { | ||
let client = this.client | ||
if (!("masters" in client)) { | ||
return client.scanIterator({MATCH: match, COUNT: count}) | ||
} | ||
return (async function* () { | ||
for (let master of client.masters) { | ||
let c = await client.nodeClient(master) | ||
for await (let keys of c.scanIterator({ | ||
COUNT: count, | ||
MATCH: match, | ||
})) { | ||
yield keys | ||
} | ||
} | ||
})() | ||
} | ||
} |
{ | ||
"name": "connect-redis", | ||
"description": "Redis session store for Connect", | ||
"version": "8.1.0", | ||
"version": "9.0.0", | ||
"author": "TJ Holowaychuk <tj@vision-media.ca>", | ||
@@ -31,22 +31,15 @@ "contributors": [ | ||
"devDependencies": { | ||
"@eslint/js": "^9.24.0", | ||
"@types/express-session": "^1.18.1", | ||
"@types/node": "^22.14.1", | ||
"@vitest/coverage-v8": "^3.1.1", | ||
"eslint": "^9.24.0", | ||
"eslint-config-prettier": "^10.1.2", | ||
"eslint-plugin-prettier": "^5.2.6", | ||
"@biomejs/biome": "^1.9.4", | ||
"@types/express-session": "^1.18.2", | ||
"@types/node": "^22.15.31", | ||
"@vitest/coverage-v8": "^3.2.3", | ||
"express-session": "^1.18.1", | ||
"ioredis": "^5.6.1", | ||
"prettier": "^3.5.3", | ||
"prettier-plugin-organize-imports": "^4.1.0", | ||
"redis": "^4.7.0", | ||
"ts-node": "^10.9.2", | ||
"typescript": "^5.8.3", | ||
"typescript-eslint": "^8.30.1", | ||
"vite": "^6.3.1", | ||
"vite-plugin-dts": "^4.5.3", | ||
"vitest": "^3.1.1" | ||
"vite": "^6.3.5", | ||
"vite-plugin-dts": "^4.5.4", | ||
"vitest": "^3.2.3" | ||
}, | ||
"peerDependencies": { | ||
"redis": ">=5", | ||
"express-session": ">=1" | ||
@@ -68,7 +61,6 @@ }, | ||
"build": "vite build", | ||
"test": "vitest run --silent --coverage", | ||
"lint": "tsc --noemit && eslint --max-warnings 0 testdata *.ts", | ||
"fmt": "prettier --write .", | ||
"fmt-check": "prettier --check ." | ||
"test": "vitest run --coverage", | ||
"lint": "tsc --noemit && biome check .", | ||
"fix": "biome check --write ." | ||
} | ||
} |
@@ -7,9 +7,4 @@ [](https://github.com/tj/connect-redis/actions/workflows/build.yml) [](https://npmjs.com/package/connect-redis)  | ||
**connect-redis** requires `express-session` to be installed and one of the following compatible Redis clients: | ||
**connect-redis** requires `express-session` and [`redis`][1]: | ||
- [`redis`][1] | ||
- [`ioredis`][2] | ||
Install with `redis`: | ||
```sh | ||
@@ -19,27 +14,5 @@ npm install redis connect-redis express-session | ||
Install with `ioredis`: | ||
```sh | ||
npm install ioredis connect-redis express-session | ||
``` | ||
## Importing | ||
**connect-redis** supports both CommonJS (`require`) and ESM (`import`) modules. | ||
Import using ESM/Typescript: | ||
```js | ||
import {RedisStore} from "connect-redis" | ||
``` | ||
Require using CommonJS: | ||
```js | ||
const {RedisStore} = require("connect-redis") | ||
``` | ||
## API | ||
Full setup using [`redis`][1] package: | ||
Full setup: | ||
@@ -78,3 +51,3 @@ ```js | ||
An instance of [`redis`][1] or [`ioredis`][2]. | ||
An instance of [`redis`][1] | ||
@@ -142,2 +115,1 @@ ##### prefix | ||
[1]: https://github.com/NodeRedis/node-redis | ||
[2]: https://github.com/luin/ioredis |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
10
-44.44%30524
-14%2
100%723
-9.06%112
-20%