Skip to content

Commit

Permalink
feat: processor transactions (#52)
Browse files Browse the repository at this point in the history
  • Loading branch information
akdasa authored Dec 16, 2022
1 parent 2bdc8c7 commit f88ec83
Show file tree
Hide file tree
Showing 7 changed files with 156 additions and 45 deletions.
38 changes: 38 additions & 0 deletions lib/commands/ExecutionStack.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { AnyCommand } from './Command'
import { Transaction } from './Transaction'


/**
* ExecutionStack is a stack of executed commands.
*/
export interface ExecutionHistoryLine {
command: AnyCommand,
transactionId: Transaction | undefined
}

/**
* ExecutionStack is a stack of executed commands.
*/
export class ExecutionStack {
private lines: ExecutionHistoryLine[] = []

public push(command: AnyCommand, transactionId?: Transaction) {
this.lines.push({ command, transactionId })
}

public includes(command: AnyCommand) {
return this.lines.map(r => r.command).includes(command)
}

public pop(): readonly AnyCommand[] {
const lastRow = this.lines.pop()
if (!lastRow) { return [] }
if (!lastRow.transactionId) { return [lastRow.command] }

const result = [lastRow.command]
while (this.lines.at(-1)?.transactionId === lastRow.transactionId) {
result.push((this.lines.pop() as ExecutionHistoryLine).command)
}
return result
}
}
59 changes: 17 additions & 42 deletions lib/commands/Processor.ts
Original file line number Diff line number Diff line change
@@ -1,44 +1,15 @@
import { AnyResult, Fail, NoResult, Ok, Result } from '@lib/core'
import { Command, AnyCommand } from './Command'
import { AnyResult, Fail, Ok, Result } from '@lib/core'
import { Command } from './Command'
import { ExecutionStack } from './ExecutionStack'
import { ProcessorResult } from './ProcessorResult'
import { Transaction } from './Transaction'


export class ProcessorResult<TCommandResult extends AnyResult> {
constructor(
public readonly processorResult: Result<boolean, string>,
public readonly commandResult: TCommandResult = NoResult as TCommandResult,
) {}

/**
* Indicates if the command is executed.
* @returns True if the command is executed, otherwise false.
* @note This property is true if the command is executed successfully or failed.
*/
get isCommandExecuted(): boolean {
return this.processorResult.isSuccess
}

/**
* Indicates if the command is executed successfully.
* @returns True if the command is executed successfully, otherwise false.
*/
get isCommandSucceeded(): boolean {
return this.commandResult.isSuccess
}

/**
* Gets the result of the executed command.
* @returns {TCommandResult['value']} Returns the result of the executed command.
*/
get value() : TCommandResult['value'] | TCommandResult['error'] {
return this.commandResult.value
}
}

/**
* Processor executes commands.
*/
export class Processor<TContext> {
private stack: AnyCommand[] = []
private stack = new ExecutionStack()

/**
* Initialize a new instance of the Processor class.
Expand All @@ -51,29 +22,33 @@ export class Processor<TContext> {
/**
* Excecute the command.
* @param command Command to process.
* @param transaction Transaction.
* @returns {ProcessorResult<TResult>} Returns the result execution.
*/
execute<TResult extends AnyResult>(
command: Command<TContext, TResult>
command: Command<TContext, TResult>,
transaction?: Transaction
): ProcessorResult<TResult> {
if (this.stack.includes(command)) {
return new ProcessorResult(Fail('Command is already executed.'))
}
this.stack.push(command)
this.stack.push(command, transaction)
const commandResult = command.execute(this.context)
return new ProcessorResult<TResult>(Ok(), commandResult)
}

/**
* Revert the last executed command.
*/
revert<TResult extends AnyResult>(): ProcessorResult<TResult> {
const command = this.stack.pop()
if (!command) {
revert(): ProcessorResult<Result<void, string>> {
const commands = this.stack.pop()
if (commands.length === 0) {
return new ProcessorResult(Fail('No command to revert.'))
}

const commandResult = command.revert(this.context)
return new ProcessorResult<TResult>(Ok(), commandResult)
for (const command of commands) {
command.revert(this.context)
}
return new ProcessorResult(Ok())
}
}
35 changes: 35 additions & 0 deletions lib/commands/ProcessorResult.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import { AnyResult, NoResult, Result } from '@lib/core'



export class ProcessorResult<TCommandResult extends AnyResult> {
constructor(
public readonly processorResult: Result<boolean, string>,
public readonly commandResult: TCommandResult = NoResult as TCommandResult
) { }

/**
* Indicates if the command is executed.
* @returns True if the command is executed, otherwise false.
* @note This property is true if the command is executed successfully or failed.
*/
get isCommandExecuted(): boolean {
return this.processorResult.isSuccess
}

/**
* Indicates if the command is executed successfully.
* @returns True if the command is executed successfully, otherwise false.
*/
get isCommandSucceeded(): boolean {
return this.commandResult.isSuccess
}

/**
* Gets the result of the executed command.
* @returns {TCommandResult['value']} Returns the result of the executed command.
*/
get value(): TCommandResult['value'] | TCommandResult['error'] {
return this.commandResult.value
}
}
12 changes: 12 additions & 0 deletions lib/commands/Transaction.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* Transaction is a marker for a group of commands.
*/


export class Transaction {
/**
* Initialize a new instance of the Transaction class.
* @param value Unique identifier of the transaction.
*/
constructor(public readonly value: string) { }
}
5 changes: 4 additions & 1 deletion lib/commands/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from './Command'
export * from './Processor'
export * from './Processor'
export * from './ExecutionStack'
export * from './ProcessorResult'
export * from './Transaction'
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@akdasa-studios/framework",
"version": "0.1.20",
"version": "0.1.21",
"description": "Framework to build every app",
"main": "dist/index.js",
"typings": "dist/index.d.ts",
Expand Down
50 changes: 49 additions & 1 deletion tests/commands/Processor.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { Result } from '@lib/core'
import { Processor, Command } from '@lib/commands'
import { Processor, Command, Transaction } from '@lib/commands'

class CalculatorContext {
constructor(public value: number) { }
Expand Down Expand Up @@ -90,4 +90,52 @@ describe('Processor', () => {
expect(result.processorResult.value).toEqual('No command to revert.')
})
})

/* -------------------------------------------------------------------------- */
/* Transactions */
/* -------------------------------------------------------------------------- */

describe('transactions', () => {
it('should revert one and last commands in transaction', () => {
const command1 = new DivCommand(2)
const transaction = new Transaction('some transaction')
processor.execute(command1, transaction)
processor.revert()
expect(context.value).toBe(100)
})

it('should revert all commands of transaction', () => {
const command1 = new DivCommand(2)
const command2 = new DivCommand(2)
const transaction = new Transaction('some transaction')
processor.execute(command1, transaction)
processor.execute(command2, transaction)
processor.revert()
expect(context.value).toBe(100)
})

it('should not revert commands out of transaction', () => {
const command1 = new DivCommand(2)
const command2 = new DivCommand(2)
const command3 = new DivCommand(2)
const transaction = new Transaction('some transaction')
processor.execute(command1)
processor.execute(command2, transaction)
processor.execute(command3, transaction)
processor.revert()
expect(context.value).toBe(50)
})

it('should not revert commands from other transaction', () => {
const command1 = new DivCommand(2)
const command2 = new DivCommand(2)
const transaction1 = new Transaction('some transaction')
const transaction2 = new Transaction('some transaction')
processor.execute(command1, transaction1)
processor.execute(command2, transaction2)

processor.revert()
expect(context.value).toBe(50)
})
})
})

0 comments on commit f88ec83

Please sign in to comment.