Skip to content

Instantly share code, notes, and snippets.

@ggoodman
Last active February 19, 2021 18:17
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ggoodman/3b2df7fb1bb272dec09ffaf97fe50756 to your computer and use it in GitHub Desktop.
Save ggoodman/3b2df7fb1bb272dec09ffaf97fe50756 to your computer and use it in GitHub Desktop.
Luggage - Because sometimes your Requests don't pack lightly.
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