Last active
July 26, 2022 16:00
-
-
Save IacopoMelani/35112dca8cb82c0dbf43c0339e6cd1f9 to your computer and use it in GitHub Desktop.
Simple and portable queue manager for handle promises sequentially
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
import { Err, ErrorCode, newError, Ok } from './util' | |
const onSessionExpired = (): void => { | |
/** handle on session expired */ | |
} | |
queueManager.registerBehavior(ErrorCode.ENOTAUTHENTICATED, | |
new Job( | |
async () => { | |
await AuthStore.actions.refreshToken() | |
if (AuthStore.state.userApp) { | |
return Ok(true) | |
} | |
return Err(newError(ErrorCode.ENOTAUTHENTICATED, "Failed to refresh token, should logout")) | |
}, | |
{ | |
rejector: onSessionExpired, | |
} | |
) | |
) |
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
import queueManager from "./QueueManager" | |
import { Err, ErrorCode, newError, Ok, PaginateResponse } from "./util" | |
const fetchRemoteData = async () => { | |
queueManager.add<string>( | |
async () => { | |
return Ok("Some result from promise") | |
}, | |
(msg: string) => { | |
// handle on success | |
}, | |
(err) => { | |
// handle on error | |
}, | |
() => { | |
// finally | |
} | |
) | |
} |
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
import { Err, Error, ErrorCode, newError, Result } from "./util" | |
type JobCall<T> = () => Promise<Result<T, Error>> | |
type JobResolver<T> = (result: T) => void | |
type JobRejector = (error: Error) => void | |
type JobAfter<T> = (res: Result<T, Error>) => void | |
/** | |
* A job that exposes a promise that can be resolved or rejected. | |
*/ | |
export class Job<T> { | |
public call: JobCall<T> | |
public resolver?: JobResolver<T> | |
public rejector?: JobRejector | |
public after?: JobAfter<T> | |
constructor(call: JobCall<T>, config: { resolver?: JobResolver<T>, rejector?: JobRejector, after?: JobAfter<T> }) { | |
this.call = call | |
this.resolver = config.resolver | |
this.rejector = config.rejector | |
this.after = config.after | |
} | |
} | |
/** | |
* A job that can be queueable with a attempts counter that rejects all run when reachs max attempts | |
*/ | |
class QueableJob<T> extends Job<T> { | |
private attempts: number | |
private maxAttempts: number | |
constructor(call: JobCall<T>, config: { resolver?: JobResolver<T>, rejector?: JobRejector, after?: JobAfter<T>, maxAttempts?: number }) { | |
super(call, { resolver: config.resolver, rejector: config.rejector, after: config.after }) | |
this.attempts = 0 | |
this.maxAttempts = config.maxAttempts || 3 | |
} | |
private incrementAttempts(): void { | |
this.attempts++ | |
} | |
public async run(): Promise<Result<any, Error>> { | |
if (!this.stillValid()) { | |
return Err(newError(ErrorCode.EMAXATTEMPTS, `Max attempts reached for job ${this.call.name}`)) | |
} | |
this.incrementAttempts() | |
return await this.call() | |
} | |
public stillValid(): bool { | |
return this.attempts < this.maxAttempts | |
} | |
} | |
/** | |
* A queue manager that manages a queue of jobs. | |
* It can be used to queue jobs and run them sequentially. | |
* Expose a BehaviorMap to manage specific errors, like JWT expired. | |
*/ | |
class QueueManager { | |
private queue: Array<QueableJob<any>> | |
private startedDispatching: bool | |
private behaviorMap: Map<ErrorCode, Job<any>> = new Map() | |
constructor() { | |
this.queue = [] | |
this.startedDispatching = false | |
} | |
public add<T>(call: JobCall<T>, resolver?: JobResolver<T>, rejector?: JobRejector, after?: JobAfter<T>) { | |
this.queue.push(new QueableJob(call, { resolver, rejector, after })) | |
if (!this.startedDispatching) { | |
this.dispatch() | |
} | |
} | |
public flush() { | |
this.queue = [] | |
} | |
public registerBehavior<T>(errorCode: ErrorCode, call: Job<T>) { | |
this.behaviorMap.set(errorCode, call) | |
} | |
private async dispatch() { | |
if (this.queue.length === 0) { | |
this.startedDispatching = false | |
return | |
} | |
this.startedDispatching = true | |
const job = this.queue.shift() | |
if (job) { | |
await this.dispatchSingleJob(job) | |
} | |
this.dispatch() | |
} | |
private async dispatchSingleJob<T>(queuedJob: QueableJob<T>) { | |
const result = await queuedJob.run() | |
if (result.isOk()) { | |
if (queuedJob.resolver) { | |
queuedJob.resolver(result.unwrap()) | |
} | |
} else { | |
const behavior = this.behaviorMap.get(result.unwrapErr().code) | |
if (behavior) { | |
const result = await behavior.call() | |
if (result.isOk()) { | |
if (behavior.resolver) { | |
behavior.resolver(result.unwrap()) | |
} | |
await this.dispatchSingleJob(queuedJob) | |
} else { | |
if (behavior.rejector) { | |
behavior.rejector(result.unwrapErr()) | |
} | |
} | |
} else if (queuedJob.rejector) { | |
queuedJob.rejector(result.unwrapErr()) | |
} | |
} | |
if(queuedJob.after) { | |
queuedJob.after(result) | |
} | |
} | |
} | |
const queueManager = new QueueManager() | |
export default queueManager |
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 type Option<T> = T | null | |
export enum ErrorCode { | |
ECONFLICT = "conflict", // conflict with current state | |
EINTERNAL = "internal", // internal error | |
EINVALID = "invalid", // invalid input | |
ENOTFOUND = "not_found", // resource not found | |
ENOTIMPLEMENTED = "not_implemented", // feature not implemented | |
EUNAUTHORIZED = "unauthorized", // access denied | |
EUNKNOWN = "unknown", // unknown error | |
EFORBIDDEN = "forbidden", // access forbidden | |
EEXISTS = "exists", // resource already exists | |
EMAXATTEMPTS = "max_attempts", // max attempts reached | |
ENOTAUTHENTICATED = "not_authenticated", // not authenticated | |
} | |
export interface Error { | |
code: ErrorCode | |
message: string | |
details: Option<string[]> | |
} | |
export const newError = (code: ErrorCode, message: string, details: Option<string[]> = null): Error => ({ | |
code, | |
message, | |
details | |
}) | |
/** | |
* Defines a result type that can be either a success or an error. | |
*/ | |
export interface Result<T, E> { | |
/** | |
* Returns `true` if the result is a success. | |
*/ | |
isOk(): bool | |
/** | |
* Returns `true` if the result is an error. | |
*/ | |
isErr(): bool | |
/** | |
* Maps the success value of the result to a new value. | |
* @param fn A function that takes the result value and returns a new result. | |
*/ | |
map<U>(fn: (value: T) => U): Result<U, E> | |
/** | |
* Maps the error value of the result to a new value. | |
* @param fn A function that takes the result error and returns a new result. | |
*/ | |
mapErr<U>(fn: (value: E) => U): Result<T, U> | |
/** | |
* Transforms the result into a new result, if result is an error then returns the default value. | |
* @param defaultValue The default value to return if the result is an error. | |
* @param fn A function that takes the success value of the result and returns a new result. | |
*/ | |
mapOr<U>(defaultValue: U, fn: (value: T) => U): U | |
/** | |
* Transforms the result into a new result, if result is an error then uses the fallback function to create a new result, otherwise uses the fn function to create a new result. | |
* @param fallback A function that takes the error value of the result and returns a new result. | |
* @param fn A function that takes the success value of the result and returns a new result. | |
*/ | |
mapOrElse<U>(fallback: (err: E) => U, fn: (value: T) => U): U | |
/** | |
* Returns the contained success value or the default value. | |
* It can thorw an error if the result is an error. | |
*/ | |
unwrap(): T | |
/** | |
* Returns the contained error value or the default value. | |
* It can thorw an error if the result is a success. | |
*/ | |
unwrapErr(): E | |
/** | |
* Returns the contained success value or the default value. | |
*/ | |
unwrapOr(defaultValue: T): T | |
/** | |
* Returns the contained success value or applies the fallback function to the error value. | |
*/ | |
unwrapOrElse(fn: (err: E) => T): T | |
} | |
/** | |
* Creates a new success result. | |
* @param value The value to return if the result is a success. | |
* @returns {Result<T, E>} A result that is a success with the given value. | |
*/ | |
export const Ok = <T, E = Error>(value: T): Result<T, E> => ({ | |
isOk: () => true, | |
isErr: () => false, | |
map: <U>(fn: (value: T) => U): Result<U, E> => Ok(fn(value)), | |
mapErr: <U>(): Result<T, U> => Ok(value), | |
mapOr: <U>(defaultValue: U, fn: (value: T) => U): U => fn(value), | |
mapOrElse: <U>(fallback: (err: E) => U, fn: (value: T) => U): U => fn(value), | |
unwrap: () => value, | |
unwrapErr: () => { | |
throw new Error("Called unwrapError on Ok") | |
}, | |
unwrapOr: () => value, | |
unwrapOrElse: () => value | |
}) | |
/** | |
* Creates a new error result. | |
* @param error The error to return if the result is an error. | |
* @returns {Result<T, E>} A result that is an error with the given error. | |
*/ | |
export const Err = <T, E = Error>(error: E): Result<T, E> => ({ | |
isOk: () => false, | |
isErr: () => true, | |
map: <U>(): Result<U, E> => Err(error), | |
mapErr: <U>(fn: (value: E) => U): Result<T, U> => Err(fn(error)), | |
mapOr: <U>(defaultValue: U): U => defaultValue, | |
mapOrElse: <U>(fallback: (err: E) => U): U => fallback(error), | |
unwrap: () => { | |
throw new Error("Called unwrap on Err") | |
}, | |
unwrapErr: () => error, | |
unwrapOr: (defaultValue: T) => defaultValue, | |
unwrapOrElse: (fn: (err: E) => T) => fn(error) | |
}) | |
export type PaginateResponse<T> = { | |
data: Array<T> | |
total_results: number | |
current_page: number | |
items_per_page: number | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment