Skip to content

Instantly share code, notes, and snippets.

@tnhu
Last active December 2, 2024 17:49
Show Gist options
  • Save tnhu/63596787bd07591bcaea3d1d9b3af1a1 to your computer and use it in GitHub Desktop.
Save tnhu/63596787bd07591bcaea3d1d9b3af1a1 to your computer and use it in GitHub Desktop.
Base class providing chainable async actions with error handling
/**
* Base class providing chainable async actions with error handling
* @template T - The type of object being operated on
*/
export abstract class ChainableActions<T> {
protected chain: Array<{ operation: string; task: () => Promise<void> }> = []
protected error: any | null = null
protected subject: T
protected errorHandler: ((error: any) => void) | null = null
protected updateHandler: ((subject: T) => void) | null = null
constructor(subject: T) {
this.subject = subject
}
/**
* Create a new instance from a subject or a promise-returning function
* @param subject - The subject or a function that returns a Promise of the subject
*/
static from<U, V extends ChainableActions<U>>(this: new (subject: U) => V, subject: U | (() => Promise<U>)): V {
if (typeof subject === 'function') {
// Create instance with a temporary subject
const instance = new this({} as U)
// Add the async initialization to the chain
instance.addToChain('initialize', async () => {
const resolvedSubject = await (subject as () => Promise<U>)()
instance.updateSubject(resolvedSubject)
})
return instance
}
return new this(subject)
}
protected addToChain(operation: string, task: () => Promise<void>) {
if (this.error) return this
this.chain.push({ operation, task })
return this
}
/**
* Update the subject being operated on
* @param subject - The new subject
*/
protected updateSubject(subject: T): this {
this.subject = subject
this.updateHandler?.(subject)
return this
}
/**
* Handle subject updates
* @param handler - Function to handle subject updates
* @param executeImmediately - Whether to execute the handler immediately with current subject (default: false)
*/
onUpdate(handler: (subject: Readonly<T>) => void, executeImmediately: boolean = false): this {
this.updateHandler = handler
if (executeImmediately) {
handler(this.subject)
}
return this
}
/**
* Handle errors in the chain
* @param handler - Function to handle the error
*/
onError(handler: (error: any) => void): this {
this.errorHandler = handler
return this
}
/**
* Execute all chained actions
* Returns this to allow re-execution of the chain
*/
async execute(): Promise<this> {
let currentOperation: string | null = null
// Execute each task in sequence
for (const { operation, task } of this.chain) {
currentOperation = operation
if (this.error) break
try {
await task()
} catch (error) {
this.error = error
break
}
}
if (this.error) {
this.errorHandler?.(this.error)
console.error(`Action failed: ${currentOperation}`, this.error)
// Reset error after handling
this.error = null
}
// Reset chain for potential re-execution
this.chain = []
return this
}
/**
* Get the current subject state
*/
getSubject(): Readonly<T> {
return this.subject
}
}
import { ChainableActions } from '../chainable-actions'
interface User {
id?: string
name?: string
email?: string
}
/**
* Chainable actions for users
*/
export class UserActions extends ChainableActions<User> {
constructor(user: User) {
super(user)
}
/**
* Update the user being operated on
* @param user - The new user object
*/
updateUser(user: User): this {
console.log('πŸ”„ Updating user:', { id: user.id, name: user.name })
return this.updateSubject(user)
}
/**
* Register a new user
*/
register(): this {
return this.addToChain('register user', async () => {
console.log('πŸ“ Registering user:', { id: this.subject.id, name: this.subject.name })
// TODO: Implement user registration logic
})
}
/**
* Add a role to the user
* @param role - The role to add
*/
addRole(role: string): this {
return this.addToChain(`add role '${role}'`, async () => {
console.log('πŸ‘€ Adding role:', { role, userId: this.subject.id, userName: this.subject.name })
// TODO: Implement role addition logic
})
}
/**
* Add a permission to the user
* @param permission - The permission to add
*/
addPermission(permission: string): this {
return this.addToChain(`add permission '${permission}'`, async () => {
console.log('πŸ”‘ Adding permission:', { permission, userId: this.subject.id, userName: this.subject.name })
// TODO: Implement permission addition logic
})
}
/**
* Join an organization
* @param orgId - The organization ID to join
*/
joinOrganization(orgId: string): this {
return this.addToChain(`join organization '${orgId}'`, async () => {
console.log('🏒 Joining organization:', { orgId, userId: this.subject.id, userName: this.subject.name })
// TODO: Implement organization joining logic
})
}
/**
* Send an invitation to join an organization
* @param orgId - The organization ID to send invitation for
*/
sendInvitationToJoinOrg(orgId: string): this {
return this.addToChain(`send invitation for organization '${orgId}'`, async () => {
console.log('βœ‰οΈ Sending organization invitation:', {
orgId,
userId: this.subject.id,
userName: this.subject.name
})
// TODO: Implement invitation sending logic
})
}
}
// Example usage:
// const user = new UserActions(userObject)
// const actions = await user
// .register()
// .addRole('admin')
// .onUpdate(user => {
// console.log('User updated:', user)
// })
// .execute()
//
// // Chain can be executed again
// await actions
// .addPermission('write')
// .execute()
// Example of another class using ChainableActions:
// class DocumentActions extends ChainableActions<Document> {
// upload(): this {
// return this.addToChain('upload document', async () => {
// // Implementation
// })
// }
//
// share(userId: string): this {
// return this.addToChain(`share with user ${userId}`, async () => {
// // Implementation
// })
// }
// }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment