Skip to content

Instantly share code, notes, and snippets.

@IacopoMelani
Last active July 26, 2022 16:00
Show Gist options
  • Save IacopoMelani/35112dca8cb82c0dbf43c0339e6cd1f9 to your computer and use it in GitHub Desktop.
Save IacopoMelani/35112dca8cb82c0dbf43c0339e6cd1f9 to your computer and use it in GitHub Desktop.
Simple and portable queue manager for handle promises sequentially
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,
}
)
)
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
}
)
}
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
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