Skip to content

Commit

Permalink
Drop legacy client support. Switch to Typescript (#377)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
wavded authored Feb 28, 2023
1 parent e5c6cd1 commit a168f99
Show file tree
Hide file tree
Showing 15 changed files with 487 additions and 431 deletions.
18 changes: 15 additions & 3 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,14 +1,26 @@
{
"root": true,
"extends": ["eslint:recommended"],
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint"],
"extends": [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"env": {
"node": true,
"es6": true
},
"parserOptions": {
"ecmaVersion": 8
"sourceType": "module",
"project": "./tsconfig.json",
"ecmaVersion": 2020
},
"rules": {
"no-console": 0
"prefer-const": 0,
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/no-empty-function": 0,
"@typescript-eslint/explicit-function-return-type": 0,
"@typescript-eslint/no-unused-vars": [2, {"argsIgnorePattern": "^_"}]
}
}
10 changes: 5 additions & 5 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,15 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
node: [14.x, 16.x, 18.x]
node: [16, 18]
name: Node v${{ matrix.node }}
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
with:
node-version: ${{ matrix.node }}
- run: sudo apt-get install -y redis-server
- run: yarn install
- run: yarn fmt-check
- run: yarn lint
- run: yarn test
- run: npm install
- run: npm run fmt-check
- run: npm run lint
- run: npm test
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,6 @@ yarn-error.log
package-lock.json
yarn.lock
.DS_Store
dump.rdb
pnpm-lock.yaml
dist
9 changes: 9 additions & 0 deletions .npmignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
testdata
coverage
.nyc_output

package-lock.json
yarn.lock
pnpm-lock.yaml

dump.rdb
5 changes: 3 additions & 2 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"tabWidth": 2,
"semi": false
"semi": false,
"bracketSpacing": false,
"plugins": ["prettier-plugin-organize-imports"]
}
1 change: 0 additions & 1 deletion index.js

This file was deleted.

204 changes: 204 additions & 0 deletions index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
import {SessionData, Store} from "express-session"

const noop = (_err?: unknown, _data?: 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 {
parse(s: string): SessionData
stringify(s: SessionData): string
}

interface RedisStoreOptions {
client: any
prefix?: string
scanCount?: number
serializer?: Serializer
ttl?: number
disableTTL?: boolean
disableTouch?: boolean
}

class RedisStore extends Store {
client: NormalizedRedisClient
prefix: string
scanCount: number
serializer: Serializer
ttl: number
disableTTL: boolean
disableTouch: boolean

constructor(opts: RedisStoreOptions) {
super()
this.prefix = opts.prefix == null ? "sess:" : opts.prefix
this.scanCount = opts.scanCount || 100
this.serializer = opts.serializer || JSON
this.ttl = opts.ttl || 86400 // One day in seconds.
this.disableTTL = opts.disableTTL || false
this.disableTouch = opts.disableTouch || false
this.client = this.normalizeClient(opts.client)
}

// Create a redis and ioredis compatible client
private normalizeClient(client: any): NormalizedRedisClient {
let isRedis = "scanIterator" 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 (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 = noop) {
let key = this.prefix + sid
try {
let data = await this.client.get(key)
if (!data) return cb()
return cb(null, this.serializer.parse(data))
} catch (err) {
return cb(err)
}
}

async set(sid: string, sess: SessionData, cb = noop) {
let key = this.prefix + sid
let ttl = this._getTTL(sess)
try {
let val = this.serializer.stringify(sess)
if (ttl > 0) {
if (this.disableTTL) await this.client.set(key, val)
else await this.client.set(key, val, ttl)
return cb()
} else {
return this.destroy(sid, cb)
}
} catch (err) {
return cb(err)
}
}

async touch(sid: string, sess: SessionData, cb = noop) {
let key = this.prefix + sid
if (this.disableTouch || this.disableTTL) return cb()
try {
await this.client.expire(key, this._getTTL(sess))
return cb()
} catch (err) {
return cb(err)
}
}

async destroy(sid: string, cb = noop) {
let key = this.prefix + sid
try {
await this.client.del([key])
return cb()
} catch (err) {
return cb(err)
}
}

async clear(cb = noop) {
try {
let keys = await this._getAllKeys()
if (!keys.length) return cb()
await this.client.del(keys)
return cb()
} catch (err) {
return cb(err)
}
}

async length(cb = noop) {
try {
let keys = await this._getAllKeys()
return cb(null, keys.length)
} catch (err) {
return cb(err)
}
}

async ids(cb = noop) {
let len = this.prefix.length
try {
let keys = await this._getAllKeys()
return cb(
null,
keys.map((k) => k.substring(len))
)
} catch (err) {
return cb(err)
}
}

async all(cb = noop) {
let len = this.prefix.length
try {
let keys = await this._getAllKeys()
if (keys.length === 0) return cb(null, [])

let data = await this.client.mget(keys)
let results = data.reduce((acc, raw, idx) => {
if (!raw) return acc
let sess = this.serializer.parse(raw) as any
sess.id = keys[idx].substring(len)
acc.push(sess)
return acc
}, [] as SessionData[])
return cb(null, results)
} catch (err) {
return cb(err)
}
}

private _getTTL(sess: SessionData) {
let ttl
if (sess && sess.cookie && sess.cookie.expires) {
let ms = Number(new Date(sess.cookie.expires)) - Date.now()
ttl = Math.ceil(ms / 1000)
} else {
ttl = this.ttl
}
return ttl
}

private async _getAllKeys() {
let pattern = this.prefix + "*"
let keys = []
for await (let key of this.client.scanIterator(pattern, this.scanCount)) {
keys.push(key)
}
return keys
}
}

export default RedisStore
Loading

0 comments on commit a168f99

Please sign in to comment.