fix: test utils events + workflow storage (#12834)
* feat(test-utils): Make event subscriber waiter robust and concurrent * feat(test-utils): Make event subscriber waiter robust and concurrent * fix workflows storage * remove timeout * Create gentle-teachers-doubt.md * revert timestamp * update changeset * fix execution loop * exit if no steps to await * typo * check next * check next * changeset * skip when async steps * wait workflow executions utils * wait workflow executions utils * wait workflow executions utils * increase timeout * break loop --------- Co-authored-by: Carlos R. L. Rodrigues <rodrigolr@gmail.com> Co-authored-by: Carlos R. L. Rodrigues <37986729+carlos-r-l-rodrigues@users.noreply.github.com>
This commit is contained in:
13 files changed
+441
-190
No files matched your search
@@ -0,0 +1,9 @@
|
|||||||
|
---
|
||||||
|
"@medusajs/utils": patch
|
||||||
|
"@medusajs/orchestration": patch
|
||||||
|
"@medusajs/test-utils": patch
|
||||||
|
"@medusajs/workflow-engine-inmemory": patch
|
||||||
|
"@medusajs/workflow-engine-redis": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
Fix/test utils events
|
||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
import path from "path"
|
import path from "path"
|
||||||
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
import { ContainerRegistrationKeys } from "@medusajs/utils"
|
||||||
|
|
||||||
jest.setTimeout(300000)
|
jest.setTimeout(100000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
medusaConfigFile: path.join(__dirname, "../../../__fixtures__/auth"),
|
medusaConfigFile: path.join(__dirname, "../../../__fixtures__/auth"),
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import {
|
|||||||
createAdminUser,
|
createAdminUser,
|
||||||
} from "../../../../helpers/create-admin-user"
|
} from "../../../../helpers/create-admin-user"
|
||||||
|
|
||||||
jest.setTimeout(30000)
|
jest.setTimeout(100000)
|
||||||
|
|
||||||
medusaIntegrationTestRunner({
|
medusaIntegrationTestRunner({
|
||||||
testSuite: ({ dbConnection, getContainer, api }) => {
|
testSuite: ({ dbConnection, getContainer, api }) => {
|
||||||
|
|||||||
@@ -841,6 +841,7 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
const execution: Promise<void | unknown>[] = []
|
const execution: Promise<void | unknown>[] = []
|
||||||
|
|
||||||
let i = 0
|
let i = 0
|
||||||
|
let hasAsyncSteps = false
|
||||||
for (const step of nextSteps.next) {
|
for (const step of nextSteps.next) {
|
||||||
const stepIndex = i++
|
const stepIndex = i++
|
||||||
if (!stepsShouldContinueExecution[stepIndex]) {
|
if (!stepsShouldContinueExecution[stepIndex]) {
|
||||||
@@ -876,12 +877,13 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
} else {
|
} else {
|
||||||
// Execute async step in background and continue the execution of the transaction
|
// Execute async step in background and continue the execution of the transaction
|
||||||
this.executeAsyncStep(promise, transaction, step, nextSteps)
|
this.executeAsyncStep(promise, transaction, step, nextSteps)
|
||||||
|
hasAsyncSteps = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
await promiseAll(execution)
|
await promiseAll(execution)
|
||||||
|
|
||||||
if (nextSteps.next.length === 0) {
|
if (nextSteps.next.length === 0 || (hasAsyncSteps && !execution.length)) {
|
||||||
continueExecution = false
|
continueExecution = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1290,6 +1292,16 @@ export class TransactionOrchestrator extends EventEmitter {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
flow.state === TransactionState.COMPENSATING ||
|
||||||
|
flow.state === TransactionState.WAITING_TO_COMPENSATE
|
||||||
|
) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_ALLOWED,
|
||||||
|
`Cannot revert a transaction that is already compensating.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
flow.state = TransactionState.WAITING_TO_COMPENSATE
|
flow.state = TransactionState.WAITING_TO_COMPENSATE
|
||||||
flow.cancelledAt = Date.now()
|
flow.cancelledAt = Date.now()
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -80,6 +80,7 @@ const SPECIAL_PROPERTIES: {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
fieldName: field.fieldName,
|
fieldName: field.fieldName,
|
||||||
defaultRaw: "now()",
|
defaultRaw: "now()",
|
||||||
|
onCreate: () => new Date(),
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
},
|
},
|
||||||
updated_at: (MikroORMEntity, field) => {
|
updated_at: (MikroORMEntity, field) => {
|
||||||
@@ -89,6 +90,8 @@ const SPECIAL_PROPERTIES: {
|
|||||||
nullable: false,
|
nullable: false,
|
||||||
fieldName: field.fieldName,
|
fieldName: field.fieldName,
|
||||||
defaultRaw: "now()",
|
defaultRaw: "now()",
|
||||||
|
onCreate: () => new Date(),
|
||||||
|
onUpdate: () => new Date(),
|
||||||
})(MikroORMEntity.prototype, field.fieldName)
|
})(MikroORMEntity.prototype, field.fieldName)
|
||||||
},
|
},
|
||||||
deleted_at: (MikroORMEntity, field, tableName) => {
|
deleted_at: (MikroORMEntity, field, tableName) => {
|
||||||
|
|||||||
@@ -39,7 +39,13 @@ describe("waitSubscribersExecution", () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
it("should reject when timeout is reached before event is fired", async () => {
|
it("should reject when timeout is reached before event is fired", async () => {
|
||||||
const waitPromise = waitSubscribersExecution(TEST_EVENT, eventBus as any)
|
const waitPromise = waitSubscribersExecution(
|
||||||
|
TEST_EVENT,
|
||||||
|
eventBus as any,
|
||||||
|
{
|
||||||
|
timeout: 5000,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
jest.advanceTimersByTime(5100)
|
jest.advanceTimersByTime(5100)
|
||||||
|
|
||||||
|
|||||||
@@ -1,82 +1,175 @@
|
|||||||
import { IEventBusModuleService } from "@medusajs/framework/types"
|
|
||||||
import { EventEmitter } from "events"
|
import { EventEmitter } from "events"
|
||||||
|
|
||||||
// Allows you to wait for all subscribers to execute for a given event. Only works with the local event bus.
|
type EventBus = {
|
||||||
export const waitSubscribersExecution = (
|
eventEmitter_: EventEmitter
|
||||||
eventName: string,
|
}
|
||||||
eventBus: IEventBusModuleService,
|
|
||||||
{
|
|
||||||
timeout = 5000,
|
|
||||||
}: {
|
|
||||||
timeout?: number
|
|
||||||
} = {}
|
|
||||||
) => {
|
|
||||||
const eventEmitter: EventEmitter = (eventBus as any).eventEmitter_
|
|
||||||
const subscriberPromises: Promise<any>[] = []
|
|
||||||
const originalListeners = eventEmitter.listeners(eventName)
|
|
||||||
let timeoutId: NodeJS.Timeout | null = null
|
|
||||||
|
|
||||||
// Create a promise that rejects after the timeout
|
type WaitSubscribersExecutionOptions = {
|
||||||
const timeoutPromise = new Promise((_, reject) => {
|
timeout?: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Map to hold pending promises for each event.
|
||||||
|
const waits = new Map<string | symbol, Promise<any>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a promise that rejects after a specified timeout.
|
||||||
|
* @param timeout - The timeout in milliseconds.
|
||||||
|
* @param eventName - The name of the event being waited on.
|
||||||
|
* @returns A tuple containing the timeout promise and a function to clear the timeout.
|
||||||
|
*/
|
||||||
|
const createTimeoutPromise = (
|
||||||
|
timeout: number,
|
||||||
|
eventName: string | symbol
|
||||||
|
): [Promise<never>, () => void] => {
|
||||||
|
let timeoutId: NodeJS.Timeout | null = null
|
||||||
|
const promise = new Promise<never>((_, reject) => {
|
||||||
timeoutId = setTimeout(() => {
|
timeoutId = setTimeout(() => {
|
||||||
reject(
|
reject(
|
||||||
new Error(
|
new Error(
|
||||||
`Timeout of ${timeout}ms exceeded while waiting for event "${eventName}"`
|
`Timeout of ${timeout}ms exceeded while waiting for event "${String(
|
||||||
|
eventName
|
||||||
|
)}"`
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}, timeout)
|
}, timeout)
|
||||||
timeoutId.unref()
|
timeoutId.unref()
|
||||||
})
|
})
|
||||||
|
return [promise, () => timeoutId && clearTimeout(timeoutId)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Core logic to wait for subscribers.
|
||||||
|
const doWaitSubscribersExecution = (
|
||||||
|
eventName: string | symbol,
|
||||||
|
eventBus: EventBus,
|
||||||
|
{ timeout = 15000 }: WaitSubscribersExecutionOptions = {}
|
||||||
|
): Promise<any> => {
|
||||||
|
const eventEmitter = eventBus.eventEmitter_
|
||||||
|
const subscriberPromises: Promise<any>[] = []
|
||||||
|
const [timeoutPromise, clearTimeout] = createTimeoutPromise(
|
||||||
|
timeout,
|
||||||
|
eventName
|
||||||
|
)
|
||||||
|
|
||||||
// If there are no existing listeners, resolve once the event happens. Otherwise, wrap the existing subscribers in a promise and resolve once they are done.
|
|
||||||
if (!eventEmitter.listeners(eventName).length) {
|
if (!eventEmitter.listeners(eventName).length) {
|
||||||
let ok
|
let ok: (value?: any) => void
|
||||||
const promise = new Promise((resolve) => {
|
const promise = new Promise((resolve) => {
|
||||||
ok = resolve
|
ok = resolve
|
||||||
})
|
})
|
||||||
|
|
||||||
subscriberPromises.push(promise)
|
subscriberPromises.push(promise)
|
||||||
eventEmitter.on(eventName, ok)
|
|
||||||
|
const newListener = async (...args: any[]) => {
|
||||||
|
eventEmitter.removeListener(eventName, newListener)
|
||||||
|
ok(...args)
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(newListener, "__isSubscribersExecutionWrapper", {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
eventEmitter.on(eventName, newListener)
|
||||||
} else {
|
} else {
|
||||||
eventEmitter.listeners(eventName).forEach((listener: any) => {
|
eventEmitter.listeners(eventName).forEach((listener: any) => {
|
||||||
|
if (listener.__isSubscribersExecutionWrapper) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
eventEmitter.removeListener(eventName, listener)
|
eventEmitter.removeListener(eventName, listener)
|
||||||
|
|
||||||
let ok, nok
|
let ok: (value?: any) => void, nok: (reason?: any) => void
|
||||||
const promise = new Promise((resolve, reject) => {
|
const promise = new Promise((resolve, reject) => {
|
||||||
ok = resolve
|
ok = resolve
|
||||||
nok = reject
|
nok = reject
|
||||||
})
|
})
|
||||||
subscriberPromises.push(promise)
|
subscriberPromises.push(promise)
|
||||||
|
|
||||||
const newListener = async (...args2) => {
|
const newListener = async (...args2: any[]) => {
|
||||||
|
// As soon as the subscriber is executed, we restore the original listener
|
||||||
|
eventEmitter.removeListener(eventName, newListener)
|
||||||
|
let listenerToAdd = listener
|
||||||
|
while (listenerToAdd.originalListener) {
|
||||||
|
listenerToAdd = listenerToAdd.originalListener
|
||||||
|
}
|
||||||
|
eventEmitter.on(eventName, listenerToAdd)
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const res = await listener.apply(eventBus, args2)
|
const res = await listener.apply(eventBus, args2)
|
||||||
|
|
||||||
ok(res)
|
ok(res)
|
||||||
|
|
||||||
return res
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
nok(error)
|
nok(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(newListener, "__isSubscribersExecutionWrapper", {
|
||||||
|
value: true,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
})
|
||||||
|
Object.defineProperty(newListener, "originalListener", {
|
||||||
|
value: listener,
|
||||||
|
configurable: true,
|
||||||
|
enumerable: false,
|
||||||
|
})
|
||||||
eventEmitter.on(eventName, newListener)
|
eventEmitter.on(eventName, newListener)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const subscribersPromise = Promise.all(subscriberPromises).finally(() => {
|
const subscribersPromise = Promise.all(subscriberPromises).finally(() => {
|
||||||
// Clear the timeout since events have been fired and handled
|
// Clear the timeout since events have been fired and handled
|
||||||
if (timeoutId !== null) {
|
clearTimeout()
|
||||||
clearTimeout(timeoutId)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Restore original event listeners
|
|
||||||
eventEmitter.removeAllListeners(eventName)
|
|
||||||
originalListeners.forEach((listener) => {
|
|
||||||
eventEmitter.on(eventName, listener as (...args: any) => void)
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// Race between the subscribers and the timeout
|
// Race between the subscribers and the timeout
|
||||||
return Promise.race([subscribersPromise, timeoutPromise])
|
return Promise.race([subscribersPromise, timeoutPromise])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Allows you to wait for all subscribers to execute for a given event.
|
||||||
|
* It ensures that concurrent waits for the same event are queued and executed sequentially.
|
||||||
|
*
|
||||||
|
* @param eventName - The name of the event to wait for.
|
||||||
|
* @param eventBus - The event bus instance.
|
||||||
|
* @param options - Options including timeout.
|
||||||
|
*/
|
||||||
|
export const waitSubscribersExecution = (
|
||||||
|
eventName: string | symbol,
|
||||||
|
eventBus: any,
|
||||||
|
options?: WaitSubscribersExecutionOptions
|
||||||
|
): Promise<any> => {
|
||||||
|
const chain = waits.get(eventName)
|
||||||
|
|
||||||
|
if (!chain) {
|
||||||
|
const newPromise = doWaitSubscribersExecution(
|
||||||
|
eventName,
|
||||||
|
eventBus,
|
||||||
|
options
|
||||||
|
).finally(() => {
|
||||||
|
// Once this chain is done, remove it from the map
|
||||||
|
// if it's still the same promise. This prevents race conditions
|
||||||
|
// where a new wait is queued before this one is removed.
|
||||||
|
if (waits.get(eventName) === newPromise) {
|
||||||
|
waits.delete(eventName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
waits.set(eventName, newPromise)
|
||||||
|
return newPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
const runner = () => {
|
||||||
|
return doWaitSubscribersExecution(eventName, eventBus, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
const newPromise = chain.then(runner, runner).finally(() => {
|
||||||
|
// Once this chain is done, remove it from the map
|
||||||
|
// if it's still the same promise. This prevents race conditions
|
||||||
|
// where a new wait is queued before this one is removed.
|
||||||
|
if (waits.get(eventName) === newPromise) {
|
||||||
|
waits.delete(eventName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
waits.set(eventName, newPromise)
|
||||||
|
|
||||||
|
return newPromise
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
|
import { MedusaContainer } from "@medusajs/types"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Waits for all workflow executions to finish. When relying on workflows but not necessarily
|
||||||
|
* waiting for them to finish, this can be used to ensure that a test is not considered done while background executions are still running and can interfere with the other tests.
|
||||||
|
* @param container - The container instance.
|
||||||
|
* @returns A promise that resolves when all workflow executions have finished.
|
||||||
|
*/
|
||||||
|
export async function waitWorkflowExecutions(container: MedusaContainer) {
|
||||||
|
const wfe = container.resolve(Modules.WORKFLOW_ENGINE, {
|
||||||
|
allowUnregistered: true,
|
||||||
|
})
|
||||||
|
if (!wfe) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
throw new Error("Timeout waiting for workflow executions to finish")
|
||||||
|
}, 10000).unref()
|
||||||
|
|
||||||
|
let waitWorkflowsToFinish = true
|
||||||
|
while (waitWorkflowsToFinish) {
|
||||||
|
const executions = await wfe.listWorkflowExecutions({
|
||||||
|
state: { $nin: ["not_started", "done", "reverted", "failed"] },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (executions.length === 0) {
|
||||||
|
waitWorkflowsToFinish = false
|
||||||
|
clearTimeout(timeout)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 50))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
startApp,
|
startApp,
|
||||||
syncLinks,
|
syncLinks,
|
||||||
} from "./medusa-test-runner-utils"
|
} from "./medusa-test-runner-utils"
|
||||||
|
import { waitWorkflowExecutions } from "./medusa-test-runner-utils/wait-workflow-executions"
|
||||||
|
|
||||||
export interface MedusaSuiteOptions {
|
export interface MedusaSuiteOptions {
|
||||||
dbConnection: any // knex instance
|
dbConnection: any // knex instance
|
||||||
@@ -34,6 +35,9 @@ export interface MedusaSuiteOptions {
|
|||||||
clientUrl: string
|
clientUrl: string
|
||||||
}
|
}
|
||||||
getMedusaApp: () => MedusaAppOutput
|
getMedusaApp: () => MedusaAppOutput
|
||||||
|
utils: {
|
||||||
|
waitWorkflowExecutions: (container: MedusaContainer) => Promise<void>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
interface TestRunnerConfig {
|
interface TestRunnerConfig {
|
||||||
@@ -268,6 +272,7 @@ class MedusaTestRunner {
|
|||||||
|
|
||||||
public async afterEach(): Promise<void> {
|
public async afterEach(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
|
await waitWorkflowExecutions(this.globalContainer as MedusaContainer)
|
||||||
await this.dbUtils.teardown({ schema: this.schema })
|
await this.dbUtils.teardown({ schema: this.schema })
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error("Error tearing down database:", error?.message)
|
logger.error("Error tearing down database:", error?.message)
|
||||||
@@ -287,6 +292,10 @@ class MedusaTestRunner {
|
|||||||
clientUrl: this.dbConfig.clientUrl,
|
clientUrl: this.dbConfig.clientUrl,
|
||||||
},
|
},
|
||||||
dbUtils: this.dbUtils,
|
dbUtils: this.dbUtils,
|
||||||
|
utils: {
|
||||||
|
waitWorkflowExecutions: () =>
|
||||||
|
waitWorkflowExecutions(this.globalContainer as MedusaContainer),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -157,6 +157,7 @@ export class InMemoryDistributedTransactionStorage
|
|||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (trx) {
|
if (trx) {
|
||||||
|
const { flow, errors } = this.storage.get(key) ?? {}
|
||||||
const { idempotent } = options ?? {}
|
const { idempotent } = options ?? {}
|
||||||
const execution = trx.execution as TransactionFlow
|
const execution = trx.execution as TransactionFlow
|
||||||
|
|
||||||
@@ -183,9 +184,9 @@ export class InMemoryDistributedTransactionStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
flow: trx.execution as TransactionFlow,
|
flow: flow ?? (trx.execution as TransactionFlow),
|
||||||
context: trx.context?.data as TransactionContext,
|
context: trx.context?.data as TransactionContext,
|
||||||
errors: trx.context?.errors as TransactionStepError[],
|
errors: errors ?? (trx.context?.errors as TransactionStepError[]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1153,7 +1153,7 @@ moduleIntegrationTestRunner<IWorkflowEngineService>({
|
|||||||
|
|
||||||
const notExpiredWorkflowId = "not-expired-workflow-" + ulid()
|
const notExpiredWorkflowId = "not-expired-workflow-" + ulid()
|
||||||
createWorkflow(
|
createWorkflow(
|
||||||
{ name: notExpiredWorkflowId, retentionTime: 1000 },
|
{ name: notExpiredWorkflowId, retentionTime: 10000 },
|
||||||
() => {
|
() => {
|
||||||
return new WorkflowResponse("not expired")
|
return new WorkflowResponse("not expired")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -291,6 +291,15 @@ export class RedisDistributedTransactionStorage
|
|||||||
.catch(() => undefined)
|
.catch(() => undefined)
|
||||||
|
|
||||||
if (trx) {
|
if (trx) {
|
||||||
|
const rawData = await this.redisClient.get(key)
|
||||||
|
|
||||||
|
let flow!: TransactionFlow, errors!: TransactionStepError[]
|
||||||
|
if (rawData) {
|
||||||
|
const data = JSON.parse(rawData)
|
||||||
|
flow = data.flow
|
||||||
|
errors = data.errors
|
||||||
|
}
|
||||||
|
|
||||||
const { idempotent } = options ?? {}
|
const { idempotent } = options ?? {}
|
||||||
const execution = trx.execution as TransactionFlow
|
const execution = trx.execution as TransactionFlow
|
||||||
|
|
||||||
@@ -317,9 +326,9 @@ export class RedisDistributedTransactionStorage
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
flow: trx.execution as TransactionFlow,
|
flow: flow ?? (trx.execution as TransactionFlow),
|
||||||
context: trx.context?.data as TransactionContext,
|
context: trx.context?.data as TransactionContext,
|
||||||
errors: trx.context?.errors as TransactionStepError[],
|
errors: errors ?? (trx.context?.errors as TransactionStepError[]),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user