chore: workflow internals improvementss (#9455)

This commit is contained in:
Adrien de Peretti
2024-10-10 09:11:56 +02:00
committed by GitHub
parent 1b9379be62
commit 34d57870ad
26 changed files with 354 additions and 206 deletions
@@ -1170,6 +1170,7 @@ medusaIntegrationTestRunner({
await deleteLineItemsWorkflow(appContainer).run({ await deleteLineItemsWorkflow(appContainer).run({
input: { input: {
cart_id: cart.id,
ids: items.map((i) => i.id), ids: items.map((i) => i.id),
}, },
throwOnError: false, throwOnError: false,
@@ -1211,6 +1212,7 @@ medusaIntegrationTestRunner({
const { errors } = await workflow.run({ const { errors } = await workflow.run({
input: { input: {
cart_id: cart.id,
ids: items.map((i) => i.id), ids: items.map((i) => i.id),
}, },
throwOnError: false, throwOnError: false,
@@ -20,7 +20,7 @@ import {
ProductStatus, ProductStatus,
PromotionRuleOperator, PromotionRuleOperator,
PromotionType, PromotionType,
RuleOperator RuleOperator,
} from "@medusajs/utils" } from "@medusajs/utils"
import { medusaIntegrationTestRunner } from "medusa-test-utils" import { medusaIntegrationTestRunner } from "medusa-test-utils"
import { import {
@@ -2131,6 +2131,7 @@ medusaIntegrationTestRunner({
) )
expect(response.status).toEqual(200) expect(response.status).toEqual(200)
expect(response.data.cart).toEqual( expect(response.data.cart).toEqual(
expect.objectContaining({ expect.objectContaining({
id: cart.id, id: cart.id,
@@ -1,8 +1,9 @@
import { MedusaError } from "@medusajs/framework/utils" import { MedusaError } from "@medusajs/framework/utils"
import { import {
WorkflowData,
createWorkflow, createWorkflow,
parallelize,
transform, transform,
WorkflowData,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
import { import {
@@ -102,13 +103,14 @@ export const addShippingMethodToWorkflow = createWorkflow(
return cart.shipping_methods.map((sm) => sm.id) return cart.shipping_methods.map((sm) => sm.id)
}) })
removeShippingMethodFromCartStep({ const [, shippingMethodsToAdd] = parallelize(
shipping_method_ids: currentShippingMethods, removeShippingMethodFromCartStep({
}) shipping_method_ids: currentShippingMethods,
}),
const shippingMethodsToAdd = addShippingMethodToCartStep({ addShippingMethodToCartStep({
shipping_methods: shippingMethodInput, shipping_methods: shippingMethodInput,
}) })
)
updateTaxLinesWorkflow.runAsStep({ updateTaxLinesWorkflow.runAsStep({
input: { input: {
@@ -49,7 +49,6 @@ export const completeCartWorkflow = createWorkflow(
name: completeCartWorkflowId, name: completeCartWorkflowId,
store: true, store: true,
idempotent: true, idempotent: true,
// 3 days of retention time
retentionTime: THREE_DAYS, retentionTime: THREE_DAYS,
}, },
( (
@@ -117,7 +117,10 @@ export const createCartWorkflow = createWorkflow(
} }
// If there is only one country in the region, we prepare a shipping address with that country's code. // If there is only one country in the region, we prepare a shipping address with that country's code.
if (!data.input.shipping_address && data.region.countries.length === 1) { if (
!data.input.shipping_address &&
data.region.countries.length === 1
) {
data_.shipping_address = { data_.shipping_address = {
country_code: data.region.countries[0].iso_2, country_code: data.region.countries[0].iso_2,
} }
@@ -4,10 +4,11 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { Modules } from "@medusajs/framework/utils" import { Modules } from "@medusajs/framework/utils"
import { import {
WorkflowData,
createStep, createStep,
createWorkflow, createWorkflow,
parallelize,
transform, transform,
WorkflowData,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { createRemoteLinkStep } from "../../common/steps/create-remote-links" import { createRemoteLinkStep } from "../../common/steps/create-remote-links"
import { useRemoteQueryStep } from "../../common/steps/use-remote-query" import { useRemoteQueryStep } from "../../common/steps/use-remote-query"
@@ -52,9 +53,10 @@ export const createPaymentCollectionForCartWorkflow = createWorkflow(
list: false, list: false,
}) })
validateCartStep({ cart }) parallelize(
validateCartStep({ cart }),
validateExistingPaymentCollectionStep({ cart }) validateExistingPaymentCollectionStep({ cart })
)
const paymentData = transform({ cart }, ({ cart }) => { const paymentData = transform({ cart }, ({ cart }) => {
return { return {
@@ -4,13 +4,13 @@ import {
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { MedusaError, PromotionActions } from "@medusajs/framework/utils" import { MedusaError, PromotionActions } from "@medusajs/framework/utils"
import { import {
WorkflowData,
WorkflowResponse,
createHook, createHook,
createWorkflow, createWorkflow,
parallelize, parallelize,
transform, transform,
when, when,
WorkflowData,
WorkflowResponse,
} from "@medusajs/framework/workflows-sdk" } from "@medusajs/framework/workflows-sdk"
import { useRemoteQueryStep } from "../../common" import { useRemoteQueryStep } from "../../common"
import { import {
@@ -138,14 +138,13 @@ export const updateCartWorkflow = createWorkflow(
list: false, list: false,
}).config({ name: "refetchcart" }) }).config({ name: "refetchcart" })
parallelize( refreshCartShippingMethodsStep({ cart })
refreshCartShippingMethodsStep({ cart }),
updateTaxLinesWorkflow.runAsStep({ updateTaxLinesWorkflow.runAsStep({
input: { input: {
cart_id: carts[0].id, cart_id: carts[0].id,
}, },
}) })
)
updateCartPromotionsWorkflow.runAsStep({ updateCartPromotionsWorkflow.runAsStep({
input: { input: {
@@ -3,8 +3,12 @@ import {
Logger, Logger,
PaymentSessionDTO, PaymentSessionDTO,
} from "@medusajs/framework/types" } from "@medusajs/framework/types"
import { ContainerRegistrationKeys, Modules } from "@medusajs/framework/utils" import {
import { StepResponse, createStep } from "@medusajs/framework/workflows-sdk" ContainerRegistrationKeys,
Modules,
promiseAll,
} from "@medusajs/framework/utils"
import { createStep, StepResponse } from "@medusajs/framework/workflows-sdk"
export interface DeletePaymentSessionStepInput { export interface DeletePaymentSessionStepInput {
ids: string[] ids: string[]
@@ -29,32 +33,42 @@ export const deletePaymentSessionsStep = createStep(
return new StepResponse([], null) return new StepResponse([], null)
} }
for (const id of ids) { const select = [
const select = [ "provider_id",
"provider_id", "currency_code",
"currency_code", "amount",
"amount", "data",
"data", "context",
"context", "payment_collection.id",
"payment_collection.id", ]
]
const [session] = await service.listPaymentSessions({ id }, { select }) const sessions = await service.listPaymentSessions({ id: ids }, { select })
const sessionMap = new Map(sessions.map((s) => [s.id, s]))
const promises: Promise<void>[] = []
for (const id of ids) {
const session = sessionMap.get(id)!
// As this requires an external method call, we will try to delete as many successful calls // As this requires an external method call, we will try to delete as many successful calls
// as possible and pass them over to the compensation step to be recreated if any of the // as possible and pass them over to the compensation step to be recreated if any of the
// payment sessions fails to delete. // payment sessions fails to delete.
try { const promise = service
await service.deletePaymentSession(id) .deletePaymentSession(id)
.then((res) => {
deleted.push(session)
})
.catch((e) => {
logger.error(
`Encountered an error when trying to delete payment session - ${id} - ${e}`
)
})
deleted.push(session) promises.push(promise)
} catch (e) {
logger.error(
`Encountered an error when trying to delete payment session - ${id} - ${e}`
)
}
} }
await promiseAll(promises)
return new StepResponse( return new StepResponse(
deleted.map((d) => d.id), deleted.map((d) => d.id),
deleted deleted
@@ -83,6 +83,14 @@ class DistributedTransaction extends EventEmitter {
private readonly context: TransactionContext = new TransactionContext() private readonly context: TransactionContext = new TransactionContext()
private static keyValueStore: IDistributedTransactionStorage private static keyValueStore: IDistributedTransactionStorage
/**
* Store data during the life cycle of the current transaction execution.
* Store non persistent data such as transformers results, temporary data, etc.
*
* @private
*/
#temporaryStorage = new Map<string, unknown>()
public static setStorage(storage: IDistributedTransactionStorage) { public static setStorage(storage: IDistributedTransactionStorage) {
this.keyValueStore = storage this.keyValueStore = storage
} }
@@ -298,6 +306,18 @@ class DistributedTransaction extends EventEmitter {
await DistributedTransaction.keyValueStore.clearStepTimeout(this, step) await DistributedTransaction.keyValueStore.clearStepTimeout(this, step)
} }
public setTemporaryData(key: string, value: unknown) {
this.#temporaryStorage.set(key, value)
}
public getTemporaryData(key: string) {
return this.#temporaryStorage.get(key)
}
public hasTemporaryData(key: string) {
return this.#temporaryStorage.has(key)
}
} }
DistributedTransaction.setStorage( DistributedTransaction.setStorage(
@@ -1,12 +1,12 @@
import { Context, LoadedModule, MedusaContainer } from "@medusajs/types" import { Context, LoadedModule, MedusaContainer } from "@medusajs/types"
import { import {
createMedusaContainer,
isDefined,
isString,
MedusaContext, MedusaContext,
MedusaContextType, MedusaContextType,
MedusaError, MedusaError,
MedusaModuleType, MedusaModuleType,
createMedusaContainer,
isDefined,
isString,
} from "@medusajs/utils" } from "@medusajs/utils"
import { asValue } from "awilix" import { asValue } from "awilix"
import { import {
@@ -107,9 +107,17 @@ export class LocalWorkflow {
return resolved return resolved
} }
const wrappableMethods = Object.getOwnPropertyNames(resolved).filter(
(key) => key !== "constructor"
)
return new Proxy(resolved, { return new Proxy(resolved, {
get: function (target, prop) { get: function (target, prop) {
if (typeof target[prop] !== "function") { const shouldWrap =
wrappableMethods.includes(prop as string) &&
typeof target[prop] === "function"
if (!shouldWrap) {
return target[prop] return target[prop]
} }
@@ -131,6 +139,7 @@ export class LocalWorkflow {
}, },
}) })
} }
return container return container
} }
@@ -369,9 +378,11 @@ export class LocalWorkflow {
await orchestrator.resume(transaction) await orchestrator.resume(transaction)
cleanUpEventListeners() try {
return transaction
return transaction } finally {
cleanUpEventListeners()
}
} }
async getRunningTransaction(uniqueTransactionId: string, context?: Context) { async getRunningTransaction(uniqueTransactionId: string, context?: Context) {
@@ -406,9 +417,11 @@ export class LocalWorkflow {
await orchestrator.cancelTransaction(transaction) await orchestrator.cancelTransaction(transaction)
cleanUpEventListeners() try {
return transaction
return transaction } finally {
cleanUpEventListeners()
}
} }
async registerStepSuccess( async registerStepSuccess(
@@ -433,9 +446,11 @@ export class LocalWorkflow {
response response
) )
cleanUpEventListeners() try {
return transaction
return transaction } finally {
cleanUpEventListeners()
}
} }
async registerStepFailure( async registerStepFailure(
@@ -459,9 +474,11 @@ export class LocalWorkflow {
handler(this.container_, context) handler(this.container_, context)
) )
cleanUpEventListeners() try {
return transaction
return transaction } finally {
cleanUpEventListeners()
}
} }
setOptions(options: Partial<TransactionModelOptions>) { setOptions(options: Partial<TransactionModelOptions>) {
+37 -17
View File
@@ -1,36 +1,56 @@
import { isObject } from "./is-object" import { isObject } from "./is-object"
import * as util from "node:util"
/** /**
* In most casees, JSON.parse(JSON.stringify(obj)) is enough to deep copy an object. * In most casees, JSON.parse(JSON.stringify(obj)) is enough to deep copy an object.
* But in some cases, it's not enough. For example, if the object contains a function or a proxy, it will be lost after JSON.parse(JSON.stringify(obj)). * But in some cases, it's not enough. For example, if the object contains a function or a proxy, it will be lost after JSON.parse(JSON.stringify(obj)).
* *
* @param obj * @param obj
* @param cache
*/ */
export function deepCopy< export function deepCopy<
T extends Record<any, any> | Record<any, any>[] = Record<any, any>, T extends Record<any, any> | Record<any, any>[] = Record<any, any>,
TOutput = T extends [] ? T[] : T TOutput = T extends [] ? T[] : T
>(obj: T): TOutput { >(obj: T, cache = new WeakMap()): TOutput {
if (obj === null || typeof obj !== "object") { if (obj === null || typeof obj !== "object") {
return obj return obj as TOutput
} }
// Handle circular references with cache
if (cache.has(obj)) {
return cache.get(obj) as TOutput
}
let copy: TOutput
// Handle arrays
if (Array.isArray(obj)) { if (Array.isArray(obj)) {
const copy: any[] = [] copy = [] as unknown as TOutput
for (let i = 0; i < obj.length; i++) { cache.set(obj, copy) // Add to cache before recursing
copy[i] = deepCopy(obj[i]) ;(obj as Array<any>).forEach((item, index) => {
} ;(copy as Array<any>)[index] = deepCopy(item, cache)
return copy as TOutput })
}
if (isObject(obj)) {
const copy: Record<any, any> = {}
for (let attr in obj) {
if (obj.hasOwnProperty(attr)) {
copy[attr] = deepCopy(obj[attr] as T)
}
}
return copy return copy
} }
return obj // Handle objects
if (isObject(obj)) {
if (util.types.isProxy(obj)) {
return obj as unknown as TOutput
}
copy = {} as TOutput
cache.set(obj, copy) // Add to cache before recursing
Object.keys(obj).forEach((key) => {
;(copy as Record<any, any>)[key] = deepCopy(
(obj as Record<any, any>)[key],
cache
)
})
return copy
}
return obj as TOutput
} }
+23 -1
View File
@@ -1,9 +1,15 @@
import { EventBusTypes } from "@medusajs/types" import {
EventBusTypes,
InternalModuleDeclaration,
MedusaContainer,
} from "@medusajs/types"
import { ulid } from "ulid" import { ulid } from "ulid"
export abstract class AbstractEventBusModuleService export abstract class AbstractEventBusModuleService
implements EventBusTypes.IEventBusModuleService implements EventBusTypes.IEventBusModuleService
{ {
protected isWorkerMode: boolean = true
protected eventToSubscribersMap_: Map< protected eventToSubscribersMap_: Map<
string | symbol, string | symbol,
EventBusTypes.SubscriberDescriptor[] EventBusTypes.SubscriberDescriptor[]
@@ -16,6 +22,14 @@ export abstract class AbstractEventBusModuleService
return this.eventToSubscribersMap_ return this.eventToSubscribersMap_
} }
protected constructor(
container: MedusaContainer,
moduleOptions = {},
moduleDeclaration: InternalModuleDeclaration
) {
this.isWorkerMode = moduleDeclaration.worker_mode !== "server"
}
abstract emit<T>( abstract emit<T>(
data: EventBusTypes.Message<T> | EventBusTypes.Message<T>[], data: EventBusTypes.Message<T> | EventBusTypes.Message<T>[],
options: Record<string, unknown> options: Record<string, unknown>
@@ -63,6 +77,10 @@ export abstract class AbstractEventBusModuleService
subscriber: EventBusTypes.Subscriber, subscriber: EventBusTypes.Subscriber,
context?: EventBusTypes.SubscriberContext context?: EventBusTypes.SubscriberContext
): this { ): this {
if (!this.isWorkerMode) {
return this
}
if (typeof subscriber !== `function`) { if (typeof subscriber !== `function`) {
throw new Error("Subscriber must be a function") throw new Error("Subscriber must be a function")
} }
@@ -88,6 +106,10 @@ export abstract class AbstractEventBusModuleService
subscriber: EventBusTypes.Subscriber, subscriber: EventBusTypes.Subscriber,
context: EventBusTypes.SubscriberContext context: EventBusTypes.SubscriberContext
): this { ): this {
if (!this.isWorkerMode) {
return this
}
if (typeof subscriber !== `function`) { if (typeof subscriber !== `function`) {
throw new Error("Subscriber must be a function") throw new Error("Subscriber must be a function")
} }
@@ -110,7 +110,7 @@ function createContextualWorkflowRunner<
events, events,
flowMetadata, flowMetadata,
] ]
const transaction = await method.apply(method, args) const transaction = await method.apply(method, args) as DistributedTransactionType
let errors = transaction.getErrors(TransactionHandlerType.INVOKE) let errors = transaction.getErrors(TransactionHandlerType.INVOKE)
@@ -150,55 +150,67 @@ export function applyStep<
const ret = { const ret = {
__type: OrchestrationUtils.SymbolWorkflowStep, __type: OrchestrationUtils.SymbolWorkflowStep,
__step__: stepName, __step__: stepName,
config: (localConfig: LocalStepConfig) => { }
const newStepName = localConfig.name ?? stepName
const newConfig = {
...stepConfig,
...localConfig,
}
delete localConfig.name const refRet = proxify(ret) as WorkflowData<TInvokeResultOutput> & {
this.handlers.set(newStepName, handler)
this.flow.replaceAction(stepConfig.uuid!, newStepName, newConfig)
this.isAsync ||= !!(newConfig.async || newConfig.compensateAsync)
ret.__step__ = newStepName
WorkflowManager.update(this.workflowId, this.flow, this.handlers)
const confRef = proxify(ret)
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
const flagSteps =
global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]
.steps
const idx = flagSteps.findIndex((a) => a.__step__ === ret.__step__)
if (idx > -1) {
flagSteps.splice(idx, 1)
}
flagSteps.push(confRef)
}
return confRef as StepFunction<TInvokeInput, TInvokeResultOutput>
},
if: ( if: (
input: any, input: any,
condition: (...args: any) => boolean | WorkflowData condition: (...args: any) => boolean | WorkflowData
): WorkflowData<TInvokeResultOutput> => { ) => WorkflowData<TInvokeResultOutput>
if (typeof condition !== "function") {
throw new Error("Condition must be a function")
}
wrapConditionalStep(input, condition, handler)
this.handlers.set(ret.__step__, handler)
return proxify(ret)
},
} }
const refRet = proxify(ret) as WorkflowData<TInvokeResultOutput> refRet.config = (
localConfig: { name?: string } & Omit<
TransactionStepsDefinition,
"next" | "uuid" | "action"
>
) => {
const newStepName = localConfig.name ?? stepName
const newConfig = {
async: false,
compensateAsync: false,
...stepConfig,
...localConfig,
}
delete localConfig.name
this.handlers.set(newStepName, handler)
this.flow.replaceAction(stepConfig.uuid!, newStepName, newConfig)
this.isAsync ||= !!(newConfig.async || newConfig.compensateAsync)
ret.__step__ = newStepName
WorkflowManager.update(this.workflowId, this.flow, this.handlers)
//const confRef = proxify(ret)
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
const flagSteps =
global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition].steps
const idx = flagSteps.findIndex((a) => a.__step__ === ret.__step__)
if (idx > -1) {
flagSteps.splice(idx, 1)
}
flagSteps.push(refRet)
}
return refRet
}
refRet.if = (
input: any,
condition: (...args: any) => boolean | WorkflowData
): WorkflowData<TInvokeResultOutput> => {
if (typeof condition !== "function") {
throw new Error("Condition must be a function")
}
wrapConditionalStep(input, condition, handler)
this.handlers.set(ret.__step__, handler)
return refRet
}
if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) { if (global[OrchestrationUtils.SymbolMedusaWorkflowComposerCondition]) {
global[ global[
@@ -1,5 +1,5 @@
import { WorkflowStepHandlerArguments } from "@medusajs/orchestration" import { WorkflowStepHandlerArguments } from "@medusajs/orchestration"
import { OrchestrationUtils, deepCopy } from "@medusajs/utils" import { OrchestrationUtils } from "@medusajs/utils"
import { ApplyStepOptions } from "../create-step" import { ApplyStepOptions } from "../create-step"
import { import {
CreateWorkflowComposerContext, CreateWorkflowComposerContext,
@@ -9,6 +9,37 @@ import {
import { resolveValue } from "./resolve-value" import { resolveValue } from "./resolve-value"
import { StepResponse } from "./step-response" import { StepResponse } from "./step-response"
function buildStepContext({
action,
stepArguments,
}: {
action: StepExecutionContext["action"]
stepArguments: WorkflowStepHandlerArguments
}) {
const metadata = stepArguments.metadata
const idempotencyKey = metadata.idempotency_key
stepArguments.context!.idempotencyKey = idempotencyKey
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action,
idempotencyKey,
attempt: metadata.attempt,
container: stepArguments.container,
metadata,
eventGroupId:
flowMetadata?.eventGroupId ?? stepArguments.context!.eventGroupId,
parentStepIdempotencyKey: flowMetadata?.parentStepIdempotencyKey as string,
transactionId: stepArguments.context!.transactionId,
context: stepArguments.context!,
}
return executionContext
}
export function createStepHandler< export function createStepHandler<
TInvokeInput, TInvokeInput,
TStepInput extends { TStepInput extends {
@@ -32,27 +63,10 @@ export function createStepHandler<
) { ) {
const handler = { const handler = {
invoke: async (stepArguments: WorkflowStepHandlerArguments) => { invoke: async (stepArguments: WorkflowStepHandlerArguments) => {
const metadata = stepArguments.metadata const executionContext = buildStepContext({
const idempotencyKey = metadata.idempotency_key
stepArguments.context!.idempotencyKey = idempotencyKey
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action: "invoke", action: "invoke",
idempotencyKey, stepArguments,
attempt: metadata.attempt, })
container: stepArguments.container,
metadata,
eventGroupId:
flowMetadata?.eventGroupId ?? stepArguments.context!.eventGroupId,
parentStepIdempotencyKey:
flowMetadata?.parentStepIdempotencyKey as string,
transactionId: stepArguments.context!.transactionId,
context: stepArguments.context!,
}
const argInput = input ? await resolveValue(input, stepArguments) : {} const argInput = input ? await resolveValue(input, stepArguments) : {}
const stepResponse: StepResponse<any, any> = await invokeFn.apply(this, [ const stepResponse: StepResponse<any, any> = await invokeFn.apply(this, [
@@ -72,31 +86,16 @@ export function createStepHandler<
}, },
compensate: compensateFn compensate: compensateFn
? async (stepArguments: WorkflowStepHandlerArguments) => { ? async (stepArguments: WorkflowStepHandlerArguments) => {
const metadata = stepArguments.metadata const executionContext = buildStepContext({
const idempotencyKey = metadata.idempotency_key
stepArguments.context!.idempotencyKey = idempotencyKey
const flowMetadata = stepArguments.transaction.getFlow()?.metadata
const executionContext: StepExecutionContext = {
workflowId: metadata.model_id,
stepName: metadata.action,
action: "compensate", action: "compensate",
idempotencyKey, stepArguments,
parentStepIdempotencyKey: })
flowMetadata?.parentStepIdempotencyKey as string,
attempt: metadata.attempt,
container: stepArguments.container,
metadata,
context: stepArguments.context!,
}
const stepOutput = (stepArguments.invoke[stepName] as any)?.output const stepOutput = (stepArguments.invoke[stepName] as any)?.output
const invokeResult = const invokeResult =
stepOutput?.__type === OrchestrationUtils.SymbolWorkflowStepResponse stepOutput?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
? stepOutput.compensateInput && ? stepOutput.compensateInput
deepCopy(stepOutput.compensateInput) : stepOutput
: stepOutput && deepCopy(stepOutput)
const args = [invokeResult, executionContext] const args = [invokeResult, executionContext]
const output = await compensateFn.apply(this, args) const output = await compensateFn.apply(this, args)
@@ -10,7 +10,7 @@ export function proxify<T>(obj: WorkflowData<any>): T {
return target[prop] return target[prop]
} }
return transform(target[prop], async function (input, context) { return transform({}, async function (_, context) {
const { invoke } = context as WorkflowTransactionContext const { invoke } = context as WorkflowTransactionContext
let output = let output =
target.__type === OrchestrationUtils.SymbolInputReference || target.__type === OrchestrationUtils.SymbolInputReference ||
@@ -19,9 +19,8 @@ export function proxify<T>(obj: WorkflowData<any>): T {
: invoke?.[obj.__step__]?.output : invoke?.[obj.__step__]?.output
output = await resolveValue(output, context) output = await resolveValue(output, context)
output = output?.[prop]
return output && JSON.parse(JSON.stringify(output)) return output?.[prop]
}) })
}, },
}) as unknown as T }) as unknown as T
@@ -3,31 +3,35 @@ import { deepCopy, OrchestrationUtils, promiseAll } from "@medusajs/utils"
async function resolveProperty(property, transactionContext) { async function resolveProperty(property, transactionContext) {
const { invoke: invokeRes } = transactionContext const { invoke: invokeRes } = transactionContext
let res
if (property?.__type === OrchestrationUtils.SymbolInputReference) { if (property?.__type === OrchestrationUtils.SymbolInputReference) {
return transactionContext.payload res = transactionContext.payload
} else if ( } else if (
property?.__type === OrchestrationUtils.SymbolMedusaWorkflowResponse property?.__type === OrchestrationUtils.SymbolMedusaWorkflowResponse
) { ) {
return resolveValue(property.$result, transactionContext) res = await resolveValue(property.$result, transactionContext)
} else if ( } else if (
property?.__type === OrchestrationUtils.SymbolWorkflowStepTransformer property?.__type === OrchestrationUtils.SymbolWorkflowStepTransformer
) { ) {
return await property.__resolver(transactionContext) res = await property.__resolver(transactionContext)
} else if (property?.__type === OrchestrationUtils.SymbolWorkflowStep) { } else if (property?.__type === OrchestrationUtils.SymbolWorkflowStep) {
const output = const output =
invokeRes[property.__step__]?.output ?? invokeRes[property.__step__] invokeRes[property.__step__]?.output ?? invokeRes[property.__step__]
if (output?.__type === OrchestrationUtils.SymbolWorkflowStepResponse) { if (output?.__type === OrchestrationUtils.SymbolWorkflowStepResponse) {
return output.output res = output.output
} else {
res = output
} }
return output
} else if ( } else if (
property?.__type === OrchestrationUtils.SymbolWorkflowStepResponse property?.__type === OrchestrationUtils.SymbolWorkflowStepResponse
) { ) {
return property.output res = property.output
} else { } else {
return property res = property
} }
return res
} }
/** /**
@@ -53,9 +57,8 @@ export async function resolveValue(input, transactionContext) {
} }
for (const key of Object.keys(inputTOUnwrap)) { for (const key of Object.keys(inputTOUnwrap)) {
parentRef[key] = await resolveProperty( parentRef[key] = deepCopy(
inputTOUnwrap[key], await resolveProperty(inputTOUnwrap[key], transactionContext)
transactionContext
) )
if (typeof parentRef[key] === "object") { if (typeof parentRef[key] === "object") {
@@ -68,8 +71,8 @@ export async function resolveValue(input, transactionContext) {
const copiedInput = const copiedInput =
input?.__type === OrchestrationUtils.SymbolWorkflowWorkflowData input?.__type === OrchestrationUtils.SymbolWorkflowWorkflowData
? deepCopy(input.output) ? input.output
: deepCopy(input) : input
const result = copiedInput?.__type const result = copiedInput?.__type
? await resolveProperty(copiedInput, transactionContext) ? await resolveProperty(copiedInput, transactionContext)
@@ -2,6 +2,11 @@ import { resolveValue } from "./helpers"
import { StepExecutionContext, WorkflowData } from "./type" import { StepExecutionContext, WorkflowData } from "./type"
import { proxify } from "./helpers/proxy" import { proxify } from "./helpers/proxy"
import { OrchestrationUtils } from "@medusajs/utils" import { OrchestrationUtils } from "@medusajs/utils"
import { ulid } from "ulid"
import {
TransactionContext,
WorkflowStepHandlerArguments,
} from "@medusajs/orchestration"
type Func1<T extends object | WorkflowData, U> = ( type Func1<T extends object | WorkflowData, U> = (
input: T extends WorkflowData<infer U> input: T extends WorkflowData<infer U>
@@ -158,16 +163,26 @@ export function transform(
values: any | any[], values: any | any[],
...functions: Function[] ...functions: Function[]
): unknown { ): unknown {
const uniqId = ulid()
const ret = { const ret = {
__id: uniqId,
__type: OrchestrationUtils.SymbolWorkflowStepTransformer, __type: OrchestrationUtils.SymbolWorkflowStepTransformer,
__resolver: undefined,
} }
const returnFn = async function (transactionContext): Promise<any> { const returnFn = async function (
const allValues = await resolveValue(values, transactionContext) // If a transformer is returned as the result of a workflow, then at this point the workflow is entirely done, in that case we have a TransactionContext
const stepValue = allValues transactionContext: WorkflowStepHandlerArguments | TransactionContext
? JSON.parse(JSON.stringify(allValues)) ): Promise<any> {
: allValues if ("transaction" in transactionContext) {
const temporaryDataKey = `${transactionContext.transaction.modelId}_${transactionContext.transaction.transactionId}_${uniqId}`
if (transactionContext.transaction.hasTemporaryData(temporaryDataKey)) {
return transactionContext.transaction.getTemporaryData(temporaryDataKey)
}
}
const stepValue = await resolveValue(values, transactionContext)
let finalResult let finalResult
for (let i = 0; i < functions.length; i++) { for (let i = 0; i < functions.length; i++) {
@@ -177,6 +192,15 @@ export function transform(
finalResult = await fn.apply(fn, [arg, transactionContext]) finalResult = await fn.apply(fn, [arg, transactionContext])
} }
if ("transaction" in transactionContext) {
const temporaryDataKey = `${transactionContext.transaction.modelId}_${transactionContext.transaction.transactionId}_${uniqId}`
transactionContext.transaction.setTemporaryData(
temporaryDataKey,
finalResult
)
}
return finalResult return finalResult
} }
@@ -52,9 +52,9 @@ export function when(input, condition) {
if (ret?.__type !== OrchestrationUtils.SymbolWorkflowStep) { if (ret?.__type !== OrchestrationUtils.SymbolWorkflowStep) {
const retStep = createStep( const retStep = createStep(
"when-then-" + ulid(), "when-then-" + ulid(),
() => new StepResponse(ret) ({ input }: { input: any }) => new StepResponse(input)
) )
returnStep = retStep() returnStep = retStep({ input: ret })
} }
for (const step of applyCondition) { for (const step of applyCondition) {
@@ -21,7 +21,7 @@ export const POST = async (
await addToCartWorkflow(req.scope).run({ await addToCartWorkflow(req.scope).run({
input: workflowInput, input: workflowInput,
}) } as any)
const updatedCart = await refetchCart( const updatedCart = await refetchCart(
req.params.id, req.params.id,
@@ -2,6 +2,7 @@ export const defaultStoreCartFields = [
"id", "id",
"currency_code", "currency_code",
"email", "email",
"region_id",
"created_at", "created_at",
"updated_at", "updated_at",
"completed_at", "completed_at",
@@ -33,6 +34,7 @@ export const defaultStoreCartFields = [
"promotions.application_method.type", "promotions.application_method.type",
"promotions.application_method.currency_code", "promotions.application_method.currency_code",
"items.id", "items.id",
"items.product.id",
"items.variant_id", "items.variant_id",
"items.product_id", "items.product_id",
"items.product.categories.id", "items.product.categories.id",
@@ -22,7 +22,7 @@ describe("LocalEventBusService", () => {
beforeEach(() => { beforeEach(() => {
jest.clearAllMocks() jest.clearAllMocks()
eventBus = new LocalEventBusService(moduleDeps as any) eventBus = new LocalEventBusService(moduleDeps as any, {}, {} as any)
eventEmitter = (eventBus as any).eventEmitter_ eventEmitter = (eventBus as any).eventEmitter_
}) })
@@ -1,6 +1,7 @@
import { import {
Event, Event,
EventBusTypes, EventBusTypes,
InternalModuleDeclaration,
Logger, Logger,
MedusaContainer, MedusaContainer,
Message, Message,
@@ -25,7 +26,11 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
protected readonly eventEmitter_: EventEmitter protected readonly eventEmitter_: EventEmitter
protected groupedEventsMap_: StagingQueueType protected groupedEventsMap_: StagingQueueType
constructor({ logger }: MedusaContainer & InjectedDependencies) { constructor(
{ logger }: MedusaContainer & InjectedDependencies,
moduleOptions = {},
moduleDeclaration: InternalModuleDeclaration
) {
// @ts-ignore // @ts-ignore
// eslint-disable-next-line prefer-rest-params // eslint-disable-next-line prefer-rest-params
super(...arguments) super(...arguments)
@@ -54,16 +59,16 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
eventData.name eventData.name
) )
if (eventListenersCount === 0) {
continue
}
if (!options.internal && !eventData.options?.internal) { if (!options.internal && !eventData.options?.internal) {
this.logger_?.info( this.logger_?.info(
`Processing ${eventData.name} which has ${eventListenersCount} subscribers` `Processing ${eventData.name} which has ${eventListenersCount} subscribers`
) )
} }
if (eventListenersCount === 0) {
continue
}
await this.groupOrEmitEvent(eventData) await this.groupOrEmitEvent(eventData)
} }
} }
@@ -114,6 +119,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
} }
subscribe(event: string | symbol, subscriber: Subscriber): this { subscribe(event: string | symbol, subscriber: Subscriber): this {
if (!this.isWorkerMode) {
return this
}
const randId = ulid() const randId = ulid()
this.storeSubscribers({ event, subscriberId: randId, subscriber }) this.storeSubscribers({ event, subscriberId: randId, subscriber })
this.eventEmitter_.on(event, async (data: Event) => { this.eventEmitter_.on(event, async (data: Event) => {
@@ -133,6 +142,10 @@ export default class LocalEventBusService extends AbstractEventBusModuleService
subscriber: Subscriber, subscriber: Subscriber,
context?: EventBusTypes.SubscriberContext context?: EventBusTypes.SubscriberContext
): this { ): this {
if (!this.isWorkerMode) {
return this
}
const existingSubscribers = this.eventToSubscribersMap_.get(event) const existingSubscribers = this.eventToSubscribersMap_.get(event)
if (existingSubscribers?.length) { if (existingSubscribers?.length) {
@@ -60,8 +60,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
}) })
// Register our worker to handle emit calls // Register our worker to handle emit calls
const shouldStartWorker = moduleDeclaration.worker_mode !== "server" if (this.isWorkerMode) {
if (shouldStartWorker) {
this.bullWorker_ = new Worker( this.bullWorker_ = new Worker(
moduleOptions.queueName ?? "events-queue", moduleOptions.queueName ?? "events-queue",
this.worker_, this.worker_,
@@ -116,7 +115,7 @@ export default class RedisEventBusService extends AbstractEventBusModuleService
// options for a particular event // options for a particular event
...eventData.options, ...eventData.options,
}, },
} } as any
}) })
} }
@@ -140,13 +140,10 @@ export class InMemoryDistributedTransactionStorage
}) })
} }
const stringifiedData = JSON.stringify(data)
const parsedData = JSON.parse(stringifiedData)
if (hasFinished && !retentionTime && !idempotent) { if (hasFinished && !retentionTime && !idempotent) {
await this.deleteFromDb(parsedData) await this.deleteFromDb(data)
} else { } else {
await this.saveToDb(parsedData) await this.saveToDb(data)
} }
if (hasFinished) { if (hasFinished) {
@@ -236,7 +236,6 @@ export class RedisDistributedTransactionStorage
} }
const stringifiedData = JSON.stringify(data) const stringifiedData = JSON.stringify(data)
const parsedData = JSON.parse(stringifiedData)
if (!hasFinished) { if (!hasFinished) {
if (ttl) { if (ttl) {
@@ -247,9 +246,9 @@ export class RedisDistributedTransactionStorage
} }
if (hasFinished && !retentionTime && !idempotent) { if (hasFinished && !retentionTime && !idempotent) {
await this.deleteFromDb(parsedData) await this.deleteFromDb(data)
} else { } else {
await this.saveToDb(parsedData) await this.saveToDb(data)
} }
if (hasFinished) { if (hasFinished) {