Skip to content

Commit

Permalink
abort() - Abort Signal aka break (#66)
Browse files Browse the repository at this point in the history
* Abort() BotAction

- interface, helper functions, and BotAction to return a signal for
  aborting

- House Cleaning / Helper Test

* ChainRunner updated with abort line signal support
* chain()() enhanced with AbortLineSignal support
* pipeRunner()() enhanced to support AbortLineSignal
* Default BotAction returns void or AbortLineSignal
* pipe()() supports AbortLineSignal
* Upgraded Utilities with AbortLineSignal support
* doWhile supports AbortLineSignal
* forAsLong supports AbortLineSignal
* pipeActionOrActions() supports AbortLineSignal
* assemblyLine supports AbortLineSignal + tweaks
* chainRunner will return AbortLineSignal.pipeValue when it's the last
  aborted BotAction line
* assemblyLine running a chain will return AbortLineSignal.pipeValue
  when it's the last aborted Botaction line
* chain will not return AbortLineSignal.pipeValue when it's the last
  aborted botaction in line
* this creates a series of steps to shift in functional complexity from
  chain -> assemblyLine (as chain) -> assembyLine(true) (as pipe) -> pipe
* forAsLong standardized & processAbortLineSignal
* processAbortLineSignal() helper unit-tested
* Utilities & Assembly Lines using processAbortLineSignal()
* tests are passing, coverage is passing
* Errors updated to support AbortLineSignal
* theory unit tests for chain/pipe on deep aborting
* additional assertion theory test
  • Loading branch information
mrWh1te authored Sep 4, 2020
1 parent 85cbe38 commit aa4fea6
Show file tree
Hide file tree
Showing 19 changed files with 1,709 additions and 330 deletions.
14 changes: 14 additions & 0 deletions src/botmation/actions/abort.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { BotAction } from "../interfaces"
import { AbortLineSignal } from '../types'
import { PipeValue } from "../types/pipe-value"
import { createAbortLineSignal } from "../helpers/abort"

/**
* BotAction to return an AbortLineSignal to be processed by an assembler for an effect of aborting assembled lines (including parent(s) if specified a number greater than 1 or 0 for all)
* @param assembledLines number of lines (from current to parent, to grandparent, to grandgrandparent, ...) to break from continuing further
* 0 means all, a way to kill a bot
* @param pipeValue works only in a Pipe assembler, optional, but if a Pipe assembler is aborted, it can return the pipeValue provided in the AbortLineSignal
or pass it along to the next assembler if the assembledLines is greater than 1 as the pipeValue is returned by the final aborted assembler
*/
export const abort = (assembledLines = 1, pipeValue?: PipeValue): BotAction<AbortLineSignal> =>
async() => createAbortLineSignal(assembledLines, pipeValue)
123 changes: 93 additions & 30 deletions src/botmation/actions/assembly-lines.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,31 +7,52 @@ import {
createEmptyPipe
} from "../helpers/pipe"
import { PipeValue } from "../types/pipe-value"
import { AbortLineSignal, isAbortLineSignal } from "../types/abort-signal"
import { processAbortLineSignal } from "../helpers/abort"

/**
* @description chain() BotAction for running a chain of BotAction's safely and optimized
* If it receives a Pipe in the injects, it will strip it out. It does not return values.
* @param actions
*/
export const chain =
(...actions: BotAction[]): BotAction =>
(...actions: BotAction<void|AbortLineSignal>[]): BotAction<void|AbortLineSignal> =>
async(page, ...injects) => {
// pipe support for running a chain inside a pipe as a real chain
// otherwise, the injects will naturally carry the pipe through the whole chain of actions in the last inject
// but, could that be desirable? A new kind of assembly line, similar to chain but carries a Pipe through (1 case ignoring BotAction returns, the other piping those return values)
if (injectsHavePipe(injects)) {
// remove pipe
if(actions.length === 1) {
await actions[0](page, ...injects.splice(0, injects.length - 1))
const returnValue = await actions[0](page, ...injects.splice(0, injects.length - 1)) // remove pipe

// by avoding the assembledLines 1 case, processAbortLineSignal() will
// by default, only return an AbortLineSignal
if (isAbortLineSignal(returnValue) && returnValue.assembledLines !== 1) {
return processAbortLineSignal(returnValue) as AbortLineSignal
}
} else {
await chainRunner(...actions)(page, ...injects.splice(0, injects.length - 1))
const returnValue = await chainRunner(...actions)(page, ...injects.splice(0, injects.length - 1)) // remove pipe

if (isAbortLineSignal(returnValue)) {
return returnValue
}
}
} else {
// run regularly in a chain, no need to remove a pipe (last inject)
if(actions.length === 1) {
await actions[0](page, ...injects)
const returnValue = await actions[0](page, ...injects)

// by avoding the assembledLines 1 case, processAbortLineSignal() will
// by default, only return an AbortLineSignal
if (isAbortLineSignal(returnValue) && returnValue.assembledLines !== 1) {
return processAbortLineSignal(returnValue) as AbortLineSignal
}
} else {
await chainRunner(...actions)(page, ...injects)
const returnValue = await chainRunner(...actions)(page, ...injects)

if (isAbortLineSignal(returnValue)) {
return returnValue
}
}
}
}
Expand All @@ -42,32 +63,45 @@ export const chain =
* @param valueToPipe
*/
export const pipe =
(valueToPipe?: any) =>
(...actions: BotAction<PipeValue|void>[]): BotAction<any> =>
(valueToPipe?: PipeValue) =>
(...actions: BotAction<PipeValue|AbortLineSignal|void>[]): BotAction<any> =>
async(page, ...injects) => {
if (injectsHavePipe(injects)) {
if (actions.length === 0) {return undefined}
if (actions.length === 1) {
let returnValue: PipeValue|AbortLineSignal|void
if (valueToPipe) {
return await actions[0](page, ...injects.splice(0, injects.length - 1), wrapValueInPipe(valueToPipe))
returnValue = await actions[0](page, ...injects.splice(0, injects.length - 1), wrapValueInPipe(valueToPipe))
} else {
return await actions[0](page, ...injects)
returnValue = await actions[0](page, ...injects)
}

if (isAbortLineSignal(returnValue)) {
return processAbortLineSignal(returnValue)
} else {
return returnValue
}
} else {
// injects only have a pipe when its ran inside a pipe, so lets return our value to flow with the pipe mechanics
if (valueToPipe) {
return (await pipeRunner(...actions)(page, ...injects.splice(0, injects.length - 1), wrapValueInPipe(valueToPipe)))
return await pipeRunner(...actions)(page, ...injects.splice(0, injects.length - 1), wrapValueInPipe(valueToPipe))
} else {
return (await pipeRunner(...actions)(page, ...injects))
return await pipeRunner(...actions)(page, ...injects)
}
}
} else {
// injects don't have a pipe, so add one:
// injects don't have a pipe, so add one
if (actions.length === 0) {return undefined}
else if (actions.length === 1) {
return await actions[0](page, ...injects, wrapValueInPipe(valueToPipe))
if (actions.length === 1) {
const returnValue = await actions[0](page, ...injects, wrapValueInPipe(valueToPipe))

if (isAbortLineSignal(returnValue)) {
return processAbortLineSignal(returnValue)
} else {
return returnValue
}
} else {
return (await pipeRunner(...actions)(page, ...injects, wrapValueInPipe(valueToPipe)))
return await pipeRunner(...actions)(page, ...injects, wrapValueInPipe(valueToPipe))
}
}
}
Expand All @@ -86,16 +120,30 @@ export const assemblyLine =
// running a pipe
if (actions.length === 0) {return undefined}
else if (actions.length === 1) {
return await actions[0](page, ...pipeInjects(injects))
const pipeActionResult = await actions[0](page, ...pipeInjects(injects))

if (isAbortLineSignal(pipeActionResult)) {
return processAbortLineSignal(pipeActionResult)
} else {
return pipeActionResult
}
} else {
return await pipeRunner(...actions)(page, ...pipeInjects(injects))
}
} else {
// running a chain
// while chains dont return pipeValues, this is an assembly line running botactions
// in a chain but it's still an assembly line, and without changing anything, you can use this
// to still work with the `pipeValue` of an AbortLineSignal, so a step-up from chain in terms of functionality but not quite pipe
// with a flag to switch into pipe, which can be great for new dev's, to explore these concepts at their own pace, one step at a time
if (actions.length === 1) {
await actions[0](page, ...injects)
} else {
await chainRunner(...actions)(page, ...injects)
const chainActionResult = await actions[0](page, ...injects)

// ignore the 1 case since then we would return the pipeValue, but chains..
if (isAbortLineSignal(chainActionResult)) {
return processAbortLineSignal(chainActionResult)
}
} else if (actions.length > 1) {
return await chainRunner(...actions)(page, ...injects)
}
}
}
Expand All @@ -107,12 +155,19 @@ export const assemblyLine =
* @param actionOrActions Botaction<PipeValue> | BotAction<PipeValue>[]
*/
export const pipeActionOrActions =
(actionOrActions: BotAction<PipeValue> | BotAction<PipeValue>[]): BotAction<PipeValue|undefined> =>
(actionOrActions: BotAction<PipeValue> | BotAction<PipeValue>[]): BotAction<PipeValue|undefined|AbortLineSignal> =>
async(page, ...injects) => {
if (Array.isArray(actionOrActions)) {
// pipe handles AbortLineSignal for itself and therefore we don't need to evaluate the signal here just return it
return await pipe()(...actionOrActions)(page, ...injects)
} else {
return await actionOrActions(page, ...pipeInjects(injects)) // simulate pipe
const singleActionResult = await actionOrActions(page, ...pipeInjects(injects)) // simulate pipe

if (isAbortLineSignal(singleActionResult)) {
return processAbortLineSignal(singleActionResult)
} else {
return singleActionResult
}
}
}

Expand All @@ -126,10 +181,15 @@ export const pipeActionOrActions =
* @param actions
*/
export const chainRunner =
(...actions: BotAction[]): BotAction =>
(...actions: BotAction<void|AbortLineSignal>[]): BotAction<void|AbortLineSignal|PipeValue> =>
async(page, ...injects) => {
let returnValue: any
for(const action of actions) {
await action(page, ...injects)
returnValue = await action(page, ...injects)

if (isAbortLineSignal(returnValue)) {
return processAbortLineSignal(returnValue)
}
}
}

Expand All @@ -139,25 +199,28 @@ export const chainRunner =
* @param actions
*/
export const pipeRunner =
<R extends PipeValue = PipeValue, P extends PipeValue = PipeValue>
(...actions: BotAction<PipeValue|void>[]): BotAction<PipeValue<R>> =>
(...actions: BotAction<PipeValue|void|AbortLineSignal>[]): BotAction<PipeValue|AbortLineSignal|undefined> =>
async(page, ...injects) => {
// Possible for last inject to be the piped value
let pipeObject: Pipe = createEmptyPipe()

// in case we are used in a chain, injects won't have a pipe at the end
if (injectsHavePipe(injects)) {
pipeObject = getInjectsPipeOrEmptyPipe<P>(injects)
pipeObject = getInjectsPipeOrEmptyPipe(injects)
injects = injects.slice(0, injects.length - 1)
}

// let piped // pipe's are closed chain-links, so nothing pipeable comes in, so data is grabbed in a pipe and shared down stream a pipe, and returns
for(const action of actions) {
const nextPipeValueOrUndefined: PipeValue|void = await action(page, ...injects, pipeObject) // typing.. botaction's async return can be void, but given how promises must resolve(), the value is actually undefined
const nextPipeValueOrUndefined: AbortLineSignal|PipeValue|void = await action(page, ...injects, pipeObject) // typing.. botaction's async return can be void, but given how promises must resolve(), the value is actually undefined

if (isAbortLineSignal(nextPipeValueOrUndefined)) {
return processAbortLineSignal(nextPipeValueOrUndefined)
}

// Bot Actions return the value removed from the pipe, and BotActionsPipe wraps it for injecting
pipeObject = wrapValueInPipe(nextPipeValueOrUndefined as PipeValue|undefined)
}

return pipeObject.value as any as PipeValue<R>
return pipeObject.value
}
2 changes: 1 addition & 1 deletion src/botmation/actions/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export const errors =
return await pipe()(...actions)(page, ...injects)
}

await chain(...(actions as BotAction[]))(page, ...injects)
return await chain(...(actions as BotAction[]))(page, ...injects)
} catch(error) {
logError('caught in ' + errorsBlockName)
console.error(error)
Expand Down
5 changes: 3 additions & 2 deletions src/botmation/actions/files.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import { inject } from "./inject"
import { errors } from "./errors"
import { forAll } from "./utilities"
import { goTo } from "./navigation"
import { unpipeInjects } from "botmation/helpers/pipe"
import { unpipeInjects } from "../helpers/pipe"
import { AbortLineSignal, PipeValue } from "../types"

/**
* @description Higher-order BotAction to inject an enriched "BotFileOptions" from an optional provided Partial of one
Expand All @@ -13,7 +14,7 @@ import { unpipeInjects } from "botmation/helpers/pipe"
* overwride the botFileOptions through their own function params
*/
export const files = (fileOptions?: Partial<BotFileOptions>) =>
(...actions: BotAction[]): BotAction =>
(...actions: BotAction<PipeValue|AbortLineSignal|void>[]): BotAction<AbortLineSignal|void> =>
inject(enrichBotFileOptionsWithDefaults(fileOptions))(
errors('files()()')(...actions)
)
Expand Down
3 changes: 2 additions & 1 deletion src/botmation/actions/inject.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { BotAction } from "../interfaces"
import { PipeValue } from "../types/pipe-value"
import { assemblyLine } from "./assembly-lines"
import { AbortLineSignal } from "../types/abort-signal"

/**
* @description Higher-order to set the first few injects for wrapped BotAction's
* @note If you need the injects to run as a pipe, wrap injects in pipe()() otherwise assemblyLine()() will run it as a chain
*/
export const inject =
(...newInjects: any[]) =>
(...actions: BotAction<PipeValue|void>[]): BotAction<any> =>
(...actions: BotAction<PipeValue|void|AbortLineSignal>[]): BotAction<any> =>
async(page, ...injects) =>
await assemblyLine()(...actions)(page, ...newInjects, ...injects)
Loading

0 comments on commit aa4fea6

Please sign in to comment.