Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove event emitter from store #347

Merged
merged 3 commits into from
Sep 20, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"singleQuote": true
}
57 changes: 27 additions & 30 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,7 @@
[![codecov](https://codecov.io/gh/hoangvvo/next-session/branch/master/graph/badge.svg)](https://codecov.io/gh/hoangvvo/next-session)
[![PRs Welcome](https://badgen.net/badge/PRs/welcome/ff5252)](CONTRIBUTING.md)

Simple *promise-based* session middleware for [Next.js](https://github.com/zeit/next.js). Also works in [micro](https://github.com/zeit/micro) or [Node.js HTTP Server](https://nodejs.org/api/http.html), [Express](https://github.com/expressjs/express), and more.

Simple _promise-based_ session middleware for [Next.js](https://github.com/zeit/next.js). Also works in [micro](https://github.com/zeit/micro) or [Node.js HTTP Server](https://nodejs.org/api/http.html), [Express](https://github.com/expressjs/express), and more.

> Also check out alternatives like [express-session](https://github.com/expressjs/session)+[next-connect](https://github.com/hoangvvo/next-connect) or [next-iron-session](https://github.com/vvo/next-iron-session) instead.
> Update: It is observed that express-session sometimes does not work properly with Next.js 11.x
Expand Down Expand Up @@ -46,20 +45,18 @@ export const config = {
api: {
externalResolver: true,
},
}
};
```

...or setting `options.autoCommit` to `false` and do `await session.commit()` (See [this](https://github.com/hoangvvo/next-session#reqsessioncommit)).

#### `{ session }`

**Note:** If you intend to call `session()` in multiple places, consider doing it only once and exporting it for elsewhere to avoid exceeded listeners.

```javascript
import { session } from 'next-session';
import nextConnect from 'next-connect';

const mySession = session({ ...options });
const mySession = session(options);

const handler = nextConnect()
.use(mySession)
Expand All @@ -68,7 +65,7 @@ const handler = nextConnect()
res.send(
`In this session, you have visited this website ${req.session.views} time(s).`
);
})
});

export default handler;
```
Expand Down Expand Up @@ -140,7 +137,7 @@ export default withSession(Page, options);
```javascript
import { applySession } from 'next-session';

export default function Page({views}) {
export default function Page({ views }) {
return (
<div>In this session, you have visited this website {views} time(s).</div>
);
Expand All @@ -151,9 +148,9 @@ export async function getServerSideProps({ req, res }) {
req.session.views = req.session.views ? req.session.views + 1 : 1;
return {
props: {
views: req.session.views
}
}
views: req.session.views,
},
};
}
```

Expand All @@ -179,21 +176,21 @@ await applySession(req, res, options);

`next-session` accepts the properties below.

| options | description | default |
|---------|-------------|---------|
| name | The name of the cookie to be read from the request and set to the response. | `sid` |
| store | The session store instance to be used. | `MemoryStore` |
| genid | The function that generates a string for a new session ID. | [`nanoid`](https://github.com/ai/nanoid) |
| encode | Transforms session ID before setting cookie. It takes the raw session ID and returns the decoded/decrypted session ID. | undefined |
| decode | Transforms session ID back while getting from cookie. It should return the encoded/encrypted session ID | undefined |
| touchAfter | Only touch after an amount of time. Disabled by default or if set to `-1`. See [touchAfter](#touchAfter). | `-1` (Disabled) |
| autoCommit | Automatically commit session. Disable this if you want to manually `session.commit()` | `true` |
| cookie.secure | Specifies the boolean value for the **Secure** `Set-Cookie` attribute. | `false` |
| cookie.httpOnly | Specifies the boolean value for the **httpOnly** `Set-Cookie` attribute. | `true` |
| cookie.path | Specifies the value for the **Path** `Set-Cookie` attribute. | `/` |
| cookie.domain | Specifies the value for the **Domain** `Set-Cookie` attribute. | unset |
| cookie.sameSite | Specifies the value for the **SameSite** `Set-Cookie` attribute. | unset |
| cookie.maxAge | **(in seconds)** Specifies the value for the **Max-Age** `Set-Cookie` attribute. | unset (Browser session) |
| options | description | default |
| --------------- | ---------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
| name | The name of the cookie to be read from the request and set to the response. | `sid` |
| store | The session store instance to be used. | `MemoryStore` |
| genid | The function that generates a string for a new session ID. | [`nanoid`](https://github.com/ai/nanoid) |
| encode | Transforms session ID before setting cookie. It takes the raw session ID and returns the decoded/decrypted session ID. | undefined |
| decode | Transforms session ID back while getting from cookie. It should return the encoded/encrypted session ID | undefined |
| touchAfter | Only touch after an amount of time. Disabled by default or if set to `-1`. See [touchAfter](#touchAfter). | `-1` (Disabled) |
| autoCommit | Automatically commit session. Disable this if you want to manually `session.commit()` | `true` |
| cookie.secure | Specifies the boolean value for the **Secure** `Set-Cookie` attribute. | `false` |
| cookie.httpOnly | Specifies the boolean value for the **httpOnly** `Set-Cookie` attribute. | `true` |
| cookie.path | Specifies the value for the **Path** `Set-Cookie` attribute. | `/` |
| cookie.domain | Specifies the value for the **Domain** `Set-Cookie` attribute. | unset |
| cookie.sameSite | Specifies the value for the **SameSite** `Set-Cookie` attribute. | unset |
| cookie.maxAge | **(in seconds)** Specifies the value for the **Max-Age** `Set-Cookie` attribute. | unset (Browser session) |

### touchAfter

Expand All @@ -203,7 +200,7 @@ In `autoCommit` mode (which is enabled by default), for optimization, a session

### encode/decode

You may supply a custom pair of function that *encode/decode* or *encrypt/decrypt* the cookie on every request.
You may supply a custom pair of function that _encode/decode_ or _encrypt/decrypt_ the cookie on every request.

```javascript
// `express-session` signing strategy
Expand Down Expand Up @@ -238,7 +235,7 @@ if (loggedOut) await req.session.destroy();

### req.session.commit()

Save the session and set neccessary headers. Return Promise. It must be called before *sending the headers (`res.writeHead`) or response (`res.send`, `res.end`, etc.)*.
Save the session and set neccessary headers. Return Promise. It must be called before _sending the headers (`res.writeHead`) or response (`res.send`, `res.end`, etc.)_.

You **must** call this if `autoCommit` is set to `false`.

Expand All @@ -254,7 +251,7 @@ The unique id that associates to the current session.

### req.session.isNew

Return *true* if the session is new.
Return _true_ if the session is new.

## Session Store

Expand Down Expand Up @@ -282,7 +279,7 @@ A compatible session store must include three functions: `set(sid, session)`, `g
// Both of the below work!

function get(sid) {
return promiseGetFn(sid)
return promiseGetFn(sid);
}

function get(sid, done) {
Expand Down
7 changes: 4 additions & 3 deletions jest.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,10 @@ module.exports = {
collectCoverageFrom: ['src/**/*'],
testMatch: ['**/*.test.ts'],
bail: true,
verbose: false,
globals: {
'ts-jest': {
diagnostics: false
}
}
diagnostics: false,
},
},
};
4 changes: 1 addition & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"prepublish": "yarn build",
"build": "tsc --outDir dist",
"lint": "eslint src --ext ts --ignore-path .gitignore",
"test": "yarn build && jest --colors --coverageReporters=text-lcov > coverage.lcov"
"test": "yarn build && jest --coverageReporters=text-lcov > coverage.lcov"
},
"repository": {
"type": "git",
Expand All @@ -43,8 +43,6 @@
"@typescript-eslint/parser": "^4.31.1",
"cookie-signature": "^1.1.0",
"eslint": "^7.32.0",
"fs-extra": "^10.0.0",
"get-port": "^5.1.1",
"jest": "^27.2.0",
"next": "11.1.2",
"react": "^17.0.2",
Expand Down
25 changes: 12 additions & 13 deletions src/compat.ts
Original file line number Diff line number Diff line change
@@ -1,34 +1,33 @@
import { EventEmitter } from 'events';
import { Store as ExpressStore } from 'express-session';
import { callbackify, inherits } from 'util';
import MemoryStore from './store/memory';

function CompatibleStore() {
// @ts-ignore
EventEmitter.call(this);
}
inherits(CompatibleStore, EventEmitter);

// no-op for compat
function expressSession(options?: any): any {}

expressSession.Store = CompatibleStore;

function CallbackMemoryStore() {}
inherits(CallbackMemoryStore, CompatibleStore);
function ExpressStore() {
// @ts-ignore
EventEmitter.call(this);
}
inherits(ExpressStore, EventEmitter);
expressSession.Store = ExpressStore;

function CallbackMemoryStore() {
// @ts-ignore
this.store = new Map();
}
inherits(CallbackMemoryStore, ExpressStore);
CallbackMemoryStore.prototype.get = callbackify(MemoryStore.prototype.get);
CallbackMemoryStore.prototype.set = callbackify(MemoryStore.prototype.set);
CallbackMemoryStore.prototype.destroy = callbackify(
MemoryStore.prototype.destroy
);
CallbackMemoryStore.prototype.all = callbackify(MemoryStore.prototype.all);

expressSession.MemoryStore = CallbackMemoryStore;

export { expressSession };

export function promisifyStore(store: ExpressStore): ExpressStore {
export function promisifyStore(store: any) {
console.warn(
'promisifyStore has been deprecated: express-session store still works without using this.'
);
Expand Down
15 changes: 0 additions & 15 deletions src/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,12 @@ import { IncomingMessage, ServerResponse } from 'http';
import { applySession } from './core';
import { Options } from './types';

let storeReady = true;

export default function session(opts?: Options) {
// store readiness
if (opts && opts.store && opts.store.on) {
opts.store.on('disconnect', () => {
storeReady = false;
});
opts.store.on('connect', () => {
storeReady = true;
});
}
return (
req: IncomingMessage,
res: ServerResponse,
next: (err?: any) => void
) => {
if (!storeReady) {
next();
return;
}
applySession(req, res, opts).then(next);
};
}
5 changes: 3 additions & 2 deletions src/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,8 +134,9 @@ export async function applySession<T = {}>(
req.session.isNew = false;
req.session.id = sessId!;
// Some store return cookie.expires as string, convert it to Date
if (typeof req.session.cookie.expires === 'string')
if (typeof req.session.cookie.expires === 'string') {
req.session.cookie.expires = new Date(req.session.cookie.expires);
}
} else {
req.session = {
cookie: {
Expand All @@ -146,7 +147,7 @@ export async function applySession<T = {}>(
secure: options.cookie?.secure || false,
...(options.cookie?.maxAge
? { maxAge: options.cookie.maxAge, expires: new Date() }
: { maxAge: null }),
: { maxAge: undefined }),
},
commit,
destroy,
Expand Down
56 changes: 20 additions & 36 deletions src/store/memory.ts
Original file line number Diff line number Diff line change
@@ -1,52 +1,36 @@
import { EventEmitter } from 'events';
import { SessionData, SessionStore } from '../types';

export default class MemoryStore extends EventEmitter implements SessionStore {
public sessions: Record<string, string> = {};
export default class MemoryStore implements SessionStore {
private store = new Map<string, string>();

constructor() {
super();
}

get(sid: string): Promise<SessionData | null> {
const self = this;

const sess = this.sessions[sid];
async get(sid: string): Promise<SessionData | null> {
const sess = this.store.get(sid);
if (sess) {
const session = JSON.parse(sess);
session.cookie.expires = session.cookie.expires
? new Date(session.cookie.expires)
: null;

const session = JSON.parse(sess, (key, value) => {
if (key === 'expires') return new Date(value);
return value;
}) as SessionData;
if (
!session.cookie.expires ||
Date.now() < session.cookie.expires.getTime()
session.cookie.expires &&
session.cookie.expires.getTime() <= Date.now()
) {
// check expires before returning
return Promise.resolve(session);
await this.destroy(sid);
return null;
}

self.destroy(sid);
return Promise.resolve(null);
return session;
}
return Promise.resolve(null);
}

set(sid: string, sess: SessionData) {
this.sessions[sid] = JSON.stringify(sess);
return Promise.resolve();
return null;
}

touch(sid: string, session: SessionData) {
return this.set(sid, session);
async set(sid: string, sess: SessionData) {
this.store.set(sid, JSON.stringify(sess));
}

all() {
return Promise.resolve(Object.values(this.sessions));
async destroy(sid: string) {
this.store.delete(sid);
}

destroy(sid: string) {
delete this.sessions[sid];
return Promise.resolve();
async touch(sid: string, sess: SessionData) {
this.store.set(sid, JSON.stringify(sess));
}
}
31 changes: 17 additions & 14 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Store as ExpressStore } from 'express-session';
import { Store as IExpressStore } from 'express-session';

export type SessionData = {
[key: string]: any;
Expand All @@ -13,36 +13,39 @@ export interface Session extends SessionData {
}

export type SessionCookieData = {
path: string;
secure: boolean;
httpOnly: boolean;
path: string;
domain?: string | undefined;
secure: boolean;
sameSite?: boolean | 'lax' | 'strict' | 'none';
} & ({ maxAge: number; expires: Date } | { maxAge: null; expires?: undefined });
} & (
| { maxAge: undefined; expires?: undefined }
| {
maxAge: number;
expires: Date;
}
);

export abstract class SessionStore {
abstract get(sid: string): Promise<SessionData | null | undefined>;
abstract set(sid: string, sess: SessionData): Promise<void>;
abstract destroy(sid: string): Promise<void>;
abstract touch?(sid: string, sess: SessionData): Promise<void>;
on?(event: string | symbol, listener: (...args: any[]) => void): this;
}

export interface Options {
name?: string;
store?: SessionStore | ExpressStore;
store?: SessionStore | IExpressStore;
genid?: () => string;
encode?: (rawSid: string) => string;
decode?: (encryptedSid: string) => string | null;
touchAfter?: number;
cookie?: {
secure?: boolean;
httpOnly?: boolean;
path?: string;
domain?: string;
sameSite?: boolean | 'lax' | 'strict' | 'none';
maxAge?: number | null;
};
cookie?: Partial<
Pick<
SessionCookieData,
'maxAge' | 'httpOnly' | 'path' | 'domain' | 'secure' | 'sameSite'
>
>;
autoCommit?: boolean;
/**
* @deprecated
Expand Down
Loading