Options
All
  • Public
  • Public/Protected
  • All
Menu

@thermopylae/lib.user-session

@thermopylae/lib.user-session

Version Node Version Documentation License: MIT

Stateful implementation of the user session.

Install

npm install @thermopylae/lib.user-session

Usage

This is a simple example of how this package can be used.

First, you need to implement storage for user sessions. In this example we will build an in-memory storage, although it's recommended to implement a persistent storage (e.g. by using Redis).

// storage.ts

import type { Seconds } from '@thermopylae/core.declarations';
import {
    PolicyBasedCache,
    AbsoluteExpirationPolicyArgumentsBundle,
    BucketGarbageCollector,
    EsMapCacheBackend,
    ProactiveExpirationPolicy,
    CacheEvent
} from '@thermopylae/lib.cache';
import type { SessionId, DeviceBase } from '@thermopylae/lib.user-session.commons';
import type { UserSessionMetaData, UserSessionsStorage } from '@thermopylae/lib.user-session';

class InMemoryUserSessionStorage implements UserSessionsStorage<DeviceBase, string> {
    private readonly cache: PolicyBasedCache<string, UserSessionMetaData<DeviceBase, string>, AbsoluteExpirationPolicyArgumentsBundle>;

    private readonly userSessions: Map<string, Set<string>>;

    public constructor() {
        const backend = new EsMapCacheBackend<string, UserSessionMetaData<DeviceBase, string>>();
        const policies = [new ProactiveExpirationPolicy<string, UserSessionMetaData<DeviceBase, string>>(new BucketGarbageCollector())];
        this.cache = new PolicyBasedCache(backend, policies);

        this.userSessions = new Map<string, Set<string>>();

        this.cache.on(CacheEvent.DELETE, (sessionIdKey) => {
            const [subject, sessionId] = InMemoryUserSessionStorage.decodeSessionIdKey(sessionIdKey);

            const sessions = this.userSessions.get(subject)!;

            sessions.delete(sessionId);
            if (sessions.size === 0) {
                this.userSessions.delete(subject);
            }
        });
    }

    public async insert(subject: string, sessionId: SessionId, metaData: UserSessionMetaData<DeviceBase, string>, ttl: Seconds): Promise<void> {
        let sessions = this.userSessions.get(subject);
        if (sessions == null) {
            sessions = new Set<string>();
            this.userSessions.set(subject, sessions);
        }

        sessions.add(sessionId);
        this.cache.set(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId), metaData, { expiresAfter: ttl });
    }

    public async read(subject: string, sessionId: SessionId): Promise<UserSessionMetaData<DeviceBase, string> | undefined> {
        return this.cache.get(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId));
    }

    public async readAll(subject: string): Promise<ReadonlyMap<SessionId, Readonly<UserSessionMetaData<DeviceBase, string>>>> {
        const sessions = this.userSessions.get(subject);
        if (sessions == null) {
            return new Map();
        }

        const sessionsMetaData = new Map<SessionId, UserSessionMetaData<DeviceBase, string>>();
        for (const sessionId of sessions) {
            sessionsMetaData.set(sessionId, this.cache.get(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId))!);
        }
        return sessionsMetaData;
    }

    public async updateAccessedAt(subject: string, sessionId: SessionId, metaData: UserSessionMetaData<DeviceBase, string>): Promise<void> {
        this.cache.set(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId), metaData);
    }

    public async delete(subject: string, sessionId: SessionId): Promise<void> {
        this.cache.del(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId));
    }

    public async deleteAll(subject: string): Promise<number> {
        const sessions = Array.from(this.userSessions.get(subject) || new Set<string>());

        for (const sessionId of sessions) {
            this.cache.del(InMemoryUserSessionStorage.sessionIdKey(subject, sessionId));
        }

        return sessions.length;
    }

    private static sessionIdKey(subject: string, sessionId: SessionId): string {
        return `${subject}:${sessionId}`;
    }

    private static decodeSessionIdKey(sessionIdKey: string): [string, SessionId] {
        return sessionIdKey.split(':') as [string, SessionId];
    }
}

export { InMemoryUserSessionStorage };

After that, we can create our UserSessionManager instance and manage user sessions.

// session.ts
import { UserSessionManager } from '@thermopylae/lib.user-session';
import { InMemoryUserSessionStorage } from './storage';

const manager = new UserSessionManager({
    idLength: 24,
    sessionTtl: 86_400, // 24h
    timeouts: {
        idle: 1_800, // 30 min
        renewal: 43_200, // 12h
        oldSessionAvailabilityAfterRenewal: 5 // 5 seconds
    },
    storage: new InMemoryUserSessionStorage(),
    renewSessionHooks: {
        onRenewMadeAlreadyFromCurrentProcess(sessionId) {
            console.warn(
                `Can't renew session '${UserSessionManager.hash(sessionId)}', because it was renewed already. Renew has been made from this NodeJS process.`
            );
        },
        onRenewMadeAlreadyFromAnotherProcess(sessionId) {
            console.warn(
                `Can't renew session '${UserSessionManager.hash(sessionId)}', because it was renewed already. Renew has been made from another NodeJS process.`
            );
        },
        onOldSessionDeleteFailure(sessionId, error) {
            console.error(`Failed to delete renewed session '${UserSessionManager.hash(sessionId)}'.`, error);
        }
    }
});

(async function main() {
    /* Create session */
    let sessionId = await manager.create('uid1', { ip: '127.0.0.1' });

    /* Read it */
    const [sessionMetaData, renewedSessionId] = await manager.read('uid1', sessionId, { ip: '127.0.0.1' });
    console.log(`Session meta data associated with session id '${sessionId}': ${JSON.stringify(sessionMetaData)}`);
    if (renewedSessionId != null) {
        console.warn(`User session was renewed and the new session id '${renewedSessionId}' needs to be sent to client.`);
        sessionId = renewedSessionId; // the old one is no longer valid
    }

    /* Read all active sessions */
    const activeSessions = await manager.readAll('uid1');
    console.log(`User with id 'uid1' has ${activeSessions.size} active sessions.`);

    /* Delete session */
    await manager.delete('uid1', sessionId);

    /* Delete all sessions */
    const deletedSessionsNo = await manager.deleteAll('uid1');
    console.info(`Deleted ${deletedSessionsNo} active sessions of user with id 'uid1'.`);
})();

API Reference

API documentation is available here.

It can also be generated by issuing the following commands:

git clone git@github.com:marinrusu1997/thermopylae.git
cd thermopylae
yarn install
yarn workspace @thermopylae/lib.user-session run doc

Author

👤 Rusu Marin

📝 License

Copyright © 2021 Rusu Marin.
This project is MIT licensed.