Skip to content

Instantly share code, notes, and snippets.

@denk0403
Last active December 22, 2023 11:16
Show Gist options
  • Save denk0403/1f17d0bc9278fd70ee1d18b202b6b8fc to your computer and use it in GitHub Desktop.
Save denk0403/1f17d0bc9278fd70ee1d18b202b6b8fc to your computer and use it in GitHub Desktop.
SyncContext and AsyncContext with Generators
/**
* A type representing any function.
*/
type AnyFunction = (...args: Array<any>) => any;
/**
* A type representing any generator function.
*/
type GenFn<T = unknown, TReturn = any, TNext = unknown> = (
...args: Array<any>
) => Generator<T, TReturn, TNext>;
/**
* A type representing the return type of a generator.
*/
type GeneratorReturnType<T extends Generator> = T extends Generator<
unknown,
infer R,
unknown
>
? R
: never;
/**
* A utility for synchronous context propagation.
*
* This class is **NOT** safe to use with async/await callbacks.
*/
class SyncContextWithDefault<T> {
private _values: Array<T>;
/**
* The current context value.
* @returns The current context value.
*/
public get value(): T {
return this._values[this._values.length - 1];
}
/**
* Creates a new `SyncContextWithDefault` with the provided default value.
* @param defaultValue - The default context value.
*/
public constructor(defaultValue: T) {
this._values = [defaultValue];
}
/**
* Invokes the provided callback with the following
* arguments and the provided value as the new context
* value for the duration of its execution.
* @param value - The new context value.
* @param callback - The callback to invoke.
* @param args - The arguments to supply to the callback.
* @returns The result of the callback.
* @example
* userContext.run(currentUser, () => {
* return UserService.getCurrentUserFriends() // accesses userContext.value
* });
*/
public run<F extends AnyFunction>(
value: T,
callback: F,
...args: Parameters<F>
): ReturnType<F> {
try {
this._values.push(value);
return callback(...args);
} finally {
this._values.pop();
}
}
/**
* Runs a generator with the provided value as the new
* context value for the duration of its execution.
*
* **NOTE:** The context value can **only** be accessed from
* within synchronous functions or other synchronous generators.
*
* @param value - The new context value.
* @param genFn - A function which produces a generator.
* @param args - The arguments to supply to the generator function.
* @returns The result of the generator.
* @example
* userContext.runGen(currentUser, function* () {
* const friends = yield UserService.getFriends(currentUser) // async
* const relatives = getCurrentUserRelativesFromFriends(friends); // accesses userContext.value
* yield BirthdayService.sendBirthdayCards(relatives); // async
* return createBirthdayCardSentResponse(relatives); // accesses userContext.value
* });
*
* userContext.runGen(currentUser, UserService.getFriendsOfType, 'relative');
*/
public async runGen<U, F extends GenFn<U, unknown, Awaited<U>>>(
value: T,
genFn: F,
...args: Parameters<F>
): Promise<GeneratorReturnType<ReturnType<F>>> {
const gen = genFn(...args);
let currentIteration = gen.next();
while (!currentIteration.done) {
const asyncResult = await currentIteration.value;
this.run(value, () => {
currentIteration = gen.next(asyncResult);
});
}
return currentIteration.value;
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment