Add CacheSession implementation & make WebSocketBus re-load the session when an event is received
This commit is contained in:
parent
8a153e3807
commit
efb9726470
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@extollo/lib",
|
"name": "@extollo/lib",
|
||||||
"version": "0.13.5",
|
"version": "0.13.6",
|
||||||
"description": "The framework library that lifts up your code.",
|
"description": "The framework library that lifts up your code.",
|
||||||
"main": "lib/index.js",
|
"main": "lib/index.js",
|
||||||
"types": "lib/index.d.ts",
|
"types": "lib/index.d.ts",
|
||||||
|
119
src/http/session/CacheSession.ts
Normal file
119
src/http/session/CacheSession.ts
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import {NoSessionKeyError, Session, SessionData, SessionNotLoadedError} from './Session'
|
||||||
|
import {Inject, Injectable} from '../../di'
|
||||||
|
import {Cache, Maybe} from '../../util'
|
||||||
|
import {Config} from '../../service/Config'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Session implementation that uses the configured Cache driver for persistence.
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class CacheSession extends Session {
|
||||||
|
@Inject()
|
||||||
|
protected readonly cache!: Cache
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly config!: Config
|
||||||
|
|
||||||
|
protected key?: string
|
||||||
|
|
||||||
|
protected data?: SessionData
|
||||||
|
|
||||||
|
protected dirty = false
|
||||||
|
|
||||||
|
forget(key: string): void {
|
||||||
|
if ( !this.data ) {
|
||||||
|
throw new SessionNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
delete this.data[key]
|
||||||
|
this.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
get(key: string, fallback?: unknown): any {
|
||||||
|
if ( !this.data ) {
|
||||||
|
throw new SessionNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.data[key] ?? fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
getData(): SessionData {
|
||||||
|
if ( !this.data ) {
|
||||||
|
throw new SessionNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return {...this.data}
|
||||||
|
}
|
||||||
|
|
||||||
|
getKey(): string {
|
||||||
|
if ( !this.key ) {
|
||||||
|
throw new NoSessionKeyError()
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.key
|
||||||
|
}
|
||||||
|
|
||||||
|
async load(): Promise<void> {
|
||||||
|
const json = await this.cache.fetch(this.formatKey())
|
||||||
|
if ( json ) {
|
||||||
|
this.data = JSON.parse(json)
|
||||||
|
} else {
|
||||||
|
this.data = {}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.dirty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
async persist(): Promise<void> {
|
||||||
|
if ( !this.dirty ) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const json = JSON.stringify(this.data)
|
||||||
|
await this.cache.put(this.formatKey(), json, this.getExpiration())
|
||||||
|
this.dirty = false
|
||||||
|
}
|
||||||
|
|
||||||
|
private getExpiration(): Maybe<Date> {
|
||||||
|
// Get the session expiration. By default, this is 4 hours.
|
||||||
|
const durationMins = this.config.safe('server.session.durationMins')
|
||||||
|
.or(4 * 60)
|
||||||
|
.integer()
|
||||||
|
|
||||||
|
if ( durationMins !== 0 ) {
|
||||||
|
const date = new Date()
|
||||||
|
date.setMinutes(date.getMinutes() + durationMins)
|
||||||
|
return date
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private formatKey(): string {
|
||||||
|
if ( !this.key ) {
|
||||||
|
throw new NoSessionKeyError()
|
||||||
|
}
|
||||||
|
|
||||||
|
const prefix = this.config.safe('app.name')
|
||||||
|
.or('Extollo')
|
||||||
|
.string()
|
||||||
|
|
||||||
|
return `${prefix}_session_${this.key}`
|
||||||
|
}
|
||||||
|
|
||||||
|
set(key: string, value: unknown): void {
|
||||||
|
if ( !this.data ) {
|
||||||
|
throw new SessionNotLoadedError()
|
||||||
|
}
|
||||||
|
|
||||||
|
this.data[key] = value
|
||||||
|
this.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setData(data: SessionData): void {
|
||||||
|
this.data = {...data}
|
||||||
|
this.dirty = true
|
||||||
|
}
|
||||||
|
|
||||||
|
setKey(key: string): void {
|
||||||
|
this.key = key
|
||||||
|
}
|
||||||
|
}
|
@ -65,6 +65,7 @@ export * from './http/kernel/module/ExecuteResolvedRouteHandlerHTTPModule'
|
|||||||
export * from './http/session/Session'
|
export * from './http/session/Session'
|
||||||
export * from './http/session/SessionFactory'
|
export * from './http/session/SessionFactory'
|
||||||
export * from './http/session/MemorySession'
|
export * from './http/session/MemorySession'
|
||||||
|
export * from './http/session/CacheSession'
|
||||||
|
|
||||||
export * from './http/Controller'
|
export * from './http/Controller'
|
||||||
|
|
||||||
|
@ -16,11 +16,25 @@ import {Bus} from './Bus'
|
|||||||
import {WebSocketCloseEvent} from '../../http/lifecycle/WebSocketCloseEvent'
|
import {WebSocketCloseEvent} from '../../http/lifecycle/WebSocketCloseEvent'
|
||||||
import {apiEvent, error} from '../../http/response/api'
|
import {apiEvent, error} from '../../http/response/api'
|
||||||
import {AsyncResource, executionAsyncId} from 'async_hooks'
|
import {AsyncResource, executionAsyncId} from 'async_hooks'
|
||||||
|
import {Session} from '../../http/session/Session'
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
||||||
awareOfContainerLifecycle: true = true
|
awareOfContainerLifecycle: true = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If true, the session will be loaded when an event is received and
|
||||||
|
* persisted after the event's handlers have executed.
|
||||||
|
*
|
||||||
|
* If an event has no handlers, the session will NOT be loaded.
|
||||||
|
*
|
||||||
|
* Use `disableSessionLoad()` to disable.
|
||||||
|
*
|
||||||
|
* @see disableSessionLoad
|
||||||
|
* @protected
|
||||||
|
*/
|
||||||
|
protected shouldLoadSessionOnEvent = true
|
||||||
|
|
||||||
@Inject()
|
@Inject()
|
||||||
protected readonly ws!: WebSocket.WebSocket
|
protected readonly ws!: WebSocket.WebSocket
|
||||||
|
|
||||||
@ -33,6 +47,9 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
|||||||
@Inject()
|
@Inject()
|
||||||
protected readonly logging!: Logging
|
protected readonly logging!: Logging
|
||||||
|
|
||||||
|
@Inject()
|
||||||
|
protected readonly session!: Session
|
||||||
|
|
||||||
public readonly uuid = uuid4()
|
public readonly uuid = uuid4()
|
||||||
|
|
||||||
private connected = false
|
private connected = false
|
||||||
@ -40,6 +57,15 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
|||||||
/** List of local subscriptions on this bus. */
|
/** List of local subscriptions on this bus. */
|
||||||
protected subscriptions: Collection<BusSubscriber<Event>> = new Collection()
|
protected subscriptions: Collection<BusSubscriber<Event>> = new Collection()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Disables re-loading & persisting the session when an event with listeners is received.
|
||||||
|
* @see shouldLoadSessionOnEvent
|
||||||
|
*/
|
||||||
|
disableSessionLoad(): this {
|
||||||
|
this.shouldLoadSessionOnEvent = false
|
||||||
|
return this
|
||||||
|
}
|
||||||
|
|
||||||
/** Get a Promise that resolves then the socket closes. */
|
/** Get a Promise that resolves then the socket closes. */
|
||||||
onClose(): Promise<WebSocketCloseEvent> {
|
onClose(): Promise<WebSocketCloseEvent> {
|
||||||
return new Promise<WebSocketCloseEvent>(res => {
|
return new Promise<WebSocketCloseEvent>(res => {
|
||||||
@ -82,9 +108,21 @@ export class WebSocketBus implements EventBus, AwareOfContainerLifecycle {
|
|||||||
protected async onMessage(message: string): Promise<void> {
|
protected async onMessage(message: string): Promise<void> {
|
||||||
const payload = await this.serial.decodeJSON<Event>(message) // FIXME validation
|
const payload = await this.serial.decodeJSON<Event>(message) // FIXME validation
|
||||||
|
|
||||||
await this.subscriptions
|
const listeners = await this.subscriptions
|
||||||
.where('eventName', '=', payload.eventName)
|
.where('eventName', '=', payload.eventName)
|
||||||
.awaitMapCall('handler', payload)
|
|
||||||
|
// If configured, re-load the session data since it may have changed outside the
|
||||||
|
// current socket's request.
|
||||||
|
if ( this.shouldLoadSessionOnEvent && listeners.isNotEmpty() ) {
|
||||||
|
await this.session.load()
|
||||||
|
}
|
||||||
|
|
||||||
|
await listeners.awaitMapCall('handler', payload)
|
||||||
|
|
||||||
|
// Persist any changes to the session for other requests.
|
||||||
|
if ( this.shouldLoadSessionOnEvent && listeners.isNotEmpty() ) {
|
||||||
|
await this.session.persist()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
up(): void {
|
up(): void {
|
||||||
|
Loading…
Reference in New Issue
Block a user