Last active
February 19, 2021 18:17
-
-
Save ggoodman/3b2df7fb1bb272dec09ffaf97fe50756 to your computer and use it in GitHub Desktop.
Luggage - Because sometimes your Requests don't pack lightly.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
export interface AsyncLuggageFactory<TLuggage, TTraveler extends {}> { | |
(obj: TTraveler): PromiseLike<TLuggage>; | |
} | |
export interface AsyncLuggage<TLuggage, TTraveler extends {}> { | |
get(obj: TTraveler): Promise<TLuggage>; | |
} | |
const swallow = () => undefined; | |
class AsyncLuggageImpl<TLuggage, TTraveler extends {}> | |
implements AsyncLuggage<TLuggage, TTraveler> { | |
private readonly luggageByTraveler = new WeakMap< | |
TTraveler, | |
Promise<TLuggage> | |
>(); | |
private readonly resolvedPromise = Promise.resolve(); | |
constructor( | |
private readonly luggageFactory: AsyncLuggageFactory<TLuggage, TTraveler> | |
) {} | |
get(obj: TTraveler) { | |
if (obj === null || typeof obj !== "object") { | |
throw new TypeError( | |
"Luggage can only be associated with Object instances" | |
); | |
} | |
let value = this.luggageByTraveler.get(obj); | |
if (typeof value === "undefined") { | |
value = this.resolvedPromise.then(() => this.luggageFactory(obj)); | |
// Trigging unhandled rejections is probably not "useful" at this layer. | |
// It would be expected for consumers of the API to handle these rejections. | |
value.catch(swallow); | |
this.luggageByTraveler.set(obj, value); | |
} | |
return value; | |
} | |
} | |
export interface SyncLuggageFactory<TLuggage, TTraveler extends {}> { | |
(obj: TTraveler): TLuggage; | |
} | |
export interface SyncLuggage<TLuggage, TTraveler extends {}> { | |
get(obj: TTraveler): TLuggage; | |
} | |
class SyncLuggageImpl<TLuggage, TTraveler extends {}> | |
implements SyncLuggage<TLuggage, TTraveler> { | |
private readonly luggageByTraveler = new WeakMap<TTraveler, TLuggage>(); | |
constructor( | |
private readonly luggageFactory: SyncLuggageFactory<TLuggage, TTraveler> | |
) {} | |
get(obj: TTraveler) { | |
if (obj === null || typeof obj !== "object") { | |
throw new TypeError( | |
"Luggage can only be associated with Object instances" | |
); | |
} | |
let value = this.luggageByTraveler.get(obj); | |
if (typeof value === "undefined") { | |
value = this.luggageFactory(obj); | |
this.luggageByTraveler.set(obj, value); | |
} | |
return value; | |
} | |
} | |
/** | |
* Create a Luggage instance that provides a mechanism to associate data with an object | |
* whose life-time is bound to that object. Obtaining the data is an asynchronous operation. | |
* | |
* @param factory function that will be called to produce a Promise resolving to the luggage | |
* associated with a given object, when not already available. | |
* | |
* @example Usage: | |
* | |
* ```js | |
* // Create a bit of 'session' luggage associated with requests | |
* const reqSession = createLuggage(req => new Session(req)); | |
* const reqUser = createLuggage(req => db.collect('users').findOne({ | |
* _id: req.session.user_id, | |
* })); | |
* | |
* // Later, in a middleware or request handler, you can read the request's | |
* // user by using `reqUser`: | |
* | |
* function userExistsMiddleware(req, res, next) { | |
* reqUser.get(req).then( | |
* user => { | |
* if (!user) { | |
* return next(new Error('No such user')); | |
* } | |
* | |
* next(); | |
* }, | |
* err => { | |
* return next(new Error('Error loading user')); | |
* } | |
* ) | |
* const session = reqSession.get(req); | |
* | |
* if (!session.isAuthenticated) { | |
* return next(new Error('Authentication required')); | |
* } | |
* | |
* return next(); | |
* } | |
* ``` | |
*/ | |
export function createLuggage<TLuggage, TTraveler extends {}>( | |
factory: SyncLuggageFactory<TLuggage, TTraveler> | |
): SyncLuggage<TLuggage, TTraveler> { | |
return new SyncLuggageImpl(factory); | |
} | |
/** | |
* Create a Luggage instance that provides a mechanism to associate data with an | |
* object whose life-time is bound to that object. | |
* | |
* @param factory function that will be called to produce the luggage associated | |
* with a given object, when not already available. | |
* | |
* @example Usage: | |
* | |
* ```js | |
* // Create a bit of 'session' luggage associated with requests | |
* const reqSession = createLuggage(req => new Session(req)); | |
* | |
* // Later, in a middleware or request handler, you can read the request's | |
* // session by using `reqSession`: | |
* | |
* function authMiddleware(req, res, next) { | |
* const session = reqSession.get(req); | |
* | |
* if (!session.isAuthenticated) { | |
* return next(new Error('Authentication required')); | |
* } | |
* | |
* return next(); | |
* } | |
* ``` | |
*/ | |
export function createAsyncLuggage<TLuggage, TTraveler extends {}>( | |
factory: AsyncLuggageFactory<TLuggage, TTraveler> | |
): AsyncLuggage<TLuggage, TTraveler> { | |
return new AsyncLuggageImpl(factory); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment