feat(locking): Locking module (#9524)
**What** - Locking Module to manage concurrency - Default `in-memory` provider
This commit is contained in:
committed by
GitHub
parent
5c9e289c4d
commit
c8b375ae2d
@@ -60,6 +60,7 @@
|
|||||||
"@medusajs/admin-vite-plugin": patch
|
"@medusajs/admin-vite-plugin": patch
|
||||||
"@medusajs/framework": patch
|
"@medusajs/framework": patch
|
||||||
"@medusajs/index": patch
|
"@medusajs/index": patch
|
||||||
|
"@medusajs/locking": patch
|
||||||
---
|
---
|
||||||
|
|
||||||
chore: Preview release changeset
|
chore: Preview release changeset
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ packages/*
|
|||||||
!packages/cache-inmemory
|
!packages/cache-inmemory
|
||||||
!packages/create-medusa-app
|
!packages/create-medusa-app
|
||||||
!packages/product
|
!packages/product
|
||||||
|
!packages/locking
|
||||||
!packages/orchestration
|
!packages/orchestration
|
||||||
!packages/workflows-sdk
|
!packages/workflows-sdk
|
||||||
!packages/core-flows
|
!packages/core-flows
|
||||||
|
|||||||
@@ -128,6 +128,7 @@ module.exports = {
|
|||||||
"./packages/modules/workflow-engine-redis/tsconfig.spec.json",
|
"./packages/modules/workflow-engine-redis/tsconfig.spec.json",
|
||||||
"./packages/modules/link-modules/tsconfig.spec.json",
|
"./packages/modules/link-modules/tsconfig.spec.json",
|
||||||
"./packages/modules/user/tsconfig.spec.json",
|
"./packages/modules/user/tsconfig.spec.json",
|
||||||
|
"./packages/modules/locking/tsconfig.spec.json",
|
||||||
|
|
||||||
"./packages/modules/providers/file-local/tsconfig.spec.json",
|
"./packages/modules/providers/file-local/tsconfig.spec.json",
|
||||||
"./packages/modules/providers/file-s3/tsconfig.spec.json",
|
"./packages/modules/providers/file-s3/tsconfig.spec.json",
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -20,6 +20,7 @@ www/**/.yarn/*
|
|||||||
.idea
|
.idea
|
||||||
.turbo
|
.turbo
|
||||||
build/**
|
build/**
|
||||||
|
dist/**
|
||||||
**/dist
|
**/dist
|
||||||
**/stats
|
**/stats
|
||||||
.favorites.json
|
.favorites.json
|
||||||
|
|||||||
@@ -1,36 +1,37 @@
|
|||||||
import { Knex } from "@mikro-orm/knex"
|
|
||||||
import { RemoteLink } from "@medusajs/modules-sdk"
|
import { RemoteLink } from "@medusajs/modules-sdk"
|
||||||
import { AwilixContainer, ResolveOptions } from "awilix"
|
|
||||||
import { Modules, ContainerRegistrationKeys } from "@medusajs/utils"
|
|
||||||
import {
|
import {
|
||||||
Logger,
|
|
||||||
ConfigModule,
|
ConfigModule,
|
||||||
ModuleImplementations,
|
IApiKeyModuleService,
|
||||||
RemoteQueryFunction,
|
|
||||||
IAuthModuleService,
|
IAuthModuleService,
|
||||||
ICacheService,
|
ICacheService,
|
||||||
ICartModuleService,
|
ICartModuleService,
|
||||||
|
ICurrencyModuleService,
|
||||||
ICustomerModuleService,
|
ICustomerModuleService,
|
||||||
IEventBusModuleService,
|
IEventBusModuleService,
|
||||||
|
IFileModuleService,
|
||||||
|
IFulfillmentModuleService,
|
||||||
IInventoryService,
|
IInventoryService,
|
||||||
|
ILockingModule,
|
||||||
|
INotificationModuleService,
|
||||||
|
IOrderModuleService,
|
||||||
IPaymentModuleService,
|
IPaymentModuleService,
|
||||||
IPricingModuleService,
|
IPricingModuleService,
|
||||||
IProductModuleService,
|
IProductModuleService,
|
||||||
IPromotionModuleService,
|
IPromotionModuleService,
|
||||||
|
IRegionModuleService,
|
||||||
ISalesChannelModuleService,
|
ISalesChannelModuleService,
|
||||||
ITaxModuleService,
|
|
||||||
IFulfillmentModuleService,
|
|
||||||
IStockLocationService,
|
IStockLocationService,
|
||||||
|
IStoreModuleService,
|
||||||
|
ITaxModuleService,
|
||||||
IUserModuleService,
|
IUserModuleService,
|
||||||
IWorkflowEngineService,
|
IWorkflowEngineService,
|
||||||
IRegionModuleService,
|
Logger,
|
||||||
IOrderModuleService,
|
ModuleImplementations,
|
||||||
IApiKeyModuleService,
|
RemoteQueryFunction,
|
||||||
IStoreModuleService,
|
|
||||||
ICurrencyModuleService,
|
|
||||||
IFileModuleService,
|
|
||||||
INotificationModuleService,
|
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
|
import { ContainerRegistrationKeys, Modules } from "@medusajs/utils"
|
||||||
|
import { Knex } from "@mikro-orm/knex"
|
||||||
|
import { AwilixContainer, ResolveOptions } from "awilix"
|
||||||
|
|
||||||
declare module "@medusajs/types" {
|
declare module "@medusajs/types" {
|
||||||
export interface ModuleImplementations {
|
export interface ModuleImplementations {
|
||||||
@@ -63,6 +64,7 @@ declare module "@medusajs/types" {
|
|||||||
[Modules.CURRENCY]: ICurrencyModuleService
|
[Modules.CURRENCY]: ICurrencyModuleService
|
||||||
[Modules.FILE]: IFileModuleService
|
[Modules.FILE]: IFileModuleService
|
||||||
[Modules.NOTIFICATION]: INotificationModuleService
|
[Modules.NOTIFICATION]: INotificationModuleService
|
||||||
|
[Modules.LOCKING]: ILockingModule
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.EVENT_BUS),
|
label: upperCaseFirst(Modules.EVENT_BUS),
|
||||||
isRequired: true,
|
isRequired: true,
|
||||||
isQueryable: false,
|
isQueryable: false,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -63,7 +63,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.PRODUCT),
|
label: upperCaseFirst(Modules.PRODUCT),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: [Modules.EVENT_BUS, "logger"],
|
dependencies: [Modules.EVENT_BUS, ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -75,7 +75,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.PRICING),
|
label: upperCaseFirst(Modules.PRICING),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: [Modules.EVENT_BUS, "logger"],
|
dependencies: [Modules.EVENT_BUS, ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -87,7 +87,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.PROMOTION),
|
label: upperCaseFirst(Modules.PROMOTION),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -99,7 +99,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.AUTH),
|
label: upperCaseFirst(Modules.AUTH),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -111,7 +111,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.WORKFLOW_ENGINE),
|
label: upperCaseFirst(Modules.WORKFLOW_ENGINE),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
__passSharedContainer: true,
|
__passSharedContainer: true,
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
@@ -124,7 +124,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.SALES_CHANNEL),
|
label: upperCaseFirst(Modules.SALES_CHANNEL),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -136,7 +136,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.FULFILLMENT),
|
label: upperCaseFirst(Modules.FULFILLMENT),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger", Modules.EVENT_BUS],
|
dependencies: [ContainerRegistrationKeys.LOGGER, Modules.EVENT_BUS],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -148,7 +148,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.CART),
|
label: upperCaseFirst(Modules.CART),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -160,7 +160,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.CUSTOMER),
|
label: upperCaseFirst(Modules.CUSTOMER),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -172,7 +172,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.PAYMENT),
|
label: upperCaseFirst(Modules.PAYMENT),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -184,7 +184,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.USER),
|
label: upperCaseFirst(Modules.USER),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: [Modules.EVENT_BUS, "logger"],
|
dependencies: [Modules.EVENT_BUS, ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -196,7 +196,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.REGION),
|
label: upperCaseFirst(Modules.REGION),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -208,7 +208,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.ORDER),
|
label: upperCaseFirst(Modules.ORDER),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger", Modules.EVENT_BUS],
|
dependencies: [ContainerRegistrationKeys.LOGGER, Modules.EVENT_BUS],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -220,7 +220,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.TAX),
|
label: upperCaseFirst(Modules.TAX),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger", Modules.EVENT_BUS],
|
dependencies: [ContainerRegistrationKeys.LOGGER, Modules.EVENT_BUS],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -232,7 +232,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.API_KEY),
|
label: upperCaseFirst(Modules.API_KEY),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -244,7 +244,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.STORE),
|
label: upperCaseFirst(Modules.STORE),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -256,7 +256,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.CURRENCY),
|
label: upperCaseFirst(Modules.CURRENCY),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -268,7 +268,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.FILE),
|
label: upperCaseFirst(Modules.FILE),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: ["logger"],
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -280,7 +280,7 @@ export const ModulesDefinition: {
|
|||||||
label: upperCaseFirst(Modules.NOTIFICATION),
|
label: upperCaseFirst(Modules.NOTIFICATION),
|
||||||
isRequired: false,
|
isRequired: false,
|
||||||
isQueryable: true,
|
isQueryable: true,
|
||||||
dependencies: [Modules.EVENT_BUS, "logger"],
|
dependencies: [Modules.EVENT_BUS, ContainerRegistrationKeys.LOGGER],
|
||||||
defaultModuleDeclaration: {
|
defaultModuleDeclaration: {
|
||||||
scope: MODULE_SCOPE.INTERNAL,
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
@@ -294,7 +294,7 @@ export const ModulesDefinition: {
|
|||||||
isQueryable: false,
|
isQueryable: false,
|
||||||
dependencies: [
|
dependencies: [
|
||||||
Modules.EVENT_BUS,
|
Modules.EVENT_BUS,
|
||||||
"logger",
|
ContainerRegistrationKeys.LOGGER,
|
||||||
ContainerRegistrationKeys.REMOTE_QUERY,
|
ContainerRegistrationKeys.REMOTE_QUERY,
|
||||||
ContainerRegistrationKeys.QUERY,
|
ContainerRegistrationKeys.QUERY,
|
||||||
],
|
],
|
||||||
@@ -303,6 +303,18 @@ export const ModulesDefinition: {
|
|||||||
resources: MODULE_RESOURCE_TYPE.SHARED,
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
[Modules.LOCKING]: {
|
||||||
|
key: Modules.LOCKING,
|
||||||
|
defaultPackage: false,
|
||||||
|
label: upperCaseFirst(Modules.LOCKING),
|
||||||
|
isRequired: false,
|
||||||
|
isQueryable: false,
|
||||||
|
dependencies: [ContainerRegistrationKeys.LOGGER],
|
||||||
|
defaultModuleDeclaration: {
|
||||||
|
scope: MODULE_SCOPE.INTERNAL,
|
||||||
|
resources: MODULE_RESOURCE_TYPE.SHARED,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MODULE_DEFINITIONS: ModuleDefinition[] =
|
export const MODULE_DEFINITIONS: ModuleDefinition[] =
|
||||||
|
|||||||
@@ -84,7 +84,7 @@ export async function loadModules(args: {
|
|||||||
sharedResourcesConfig,
|
sharedResourcesConfig,
|
||||||
migrationOnly = false,
|
migrationOnly = false,
|
||||||
loaderOnly = false,
|
loaderOnly = false,
|
||||||
workerMode = "server" as ModuleBootstrapOptions["workerMode"],
|
workerMode = "shared" as ModuleBootstrapOptions["workerMode"],
|
||||||
} = args
|
} = args
|
||||||
|
|
||||||
const allModules = {} as any
|
const allModules = {} as any
|
||||||
@@ -307,7 +307,7 @@ async function MedusaApp_({
|
|||||||
injectedDependencies = {},
|
injectedDependencies = {},
|
||||||
migrationOnly = false,
|
migrationOnly = false,
|
||||||
loaderOnly = false,
|
loaderOnly = false,
|
||||||
workerMode = "server",
|
workerMode = "shared",
|
||||||
}: MedusaAppOptions & {
|
}: MedusaAppOptions & {
|
||||||
migrationOnly?: boolean
|
migrationOnly?: boolean
|
||||||
} = {}): Promise<MedusaAppOutput> {
|
} = {}): Promise<MedusaAppOutput> {
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ export * from "./index-data"
|
|||||||
export * from "./inventory"
|
export * from "./inventory"
|
||||||
export * from "./joiner"
|
export * from "./joiner"
|
||||||
export * from "./link-modules"
|
export * from "./link-modules"
|
||||||
|
export * from "./locking"
|
||||||
export * from "./logger"
|
export * from "./logger"
|
||||||
export * from "./modules-sdk"
|
export * from "./modules-sdk"
|
||||||
export * from "./notification"
|
export * from "./notification"
|
||||||
|
|||||||
69
packages/core/types/src/locking/index.ts
Normal file
69
packages/core/types/src/locking/index.ts
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { Context } from "../shared-context"
|
||||||
|
|
||||||
|
export interface ILockingProvider {
|
||||||
|
execute<T>(
|
||||||
|
keys: string | string[],
|
||||||
|
job: () => Promise<T>,
|
||||||
|
args?: {
|
||||||
|
timeout?: number
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<T>
|
||||||
|
acquire(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
expire?: number
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
release(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<boolean>
|
||||||
|
releaseAll(
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ILockingModule {
|
||||||
|
execute<T>(
|
||||||
|
keys: string | string[],
|
||||||
|
job: () => Promise<T>,
|
||||||
|
args?: {
|
||||||
|
timeout?: number
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<T>
|
||||||
|
acquire(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
expire?: number
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
release(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<boolean>
|
||||||
|
releaseAll(
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext?: Context
|
||||||
|
): Promise<void>
|
||||||
|
}
|
||||||
@@ -8,4 +8,5 @@ export type ModuleProvider = {
|
|||||||
resolve: string | ModuleProviderExports
|
resolve: string | ModuleProviderExports
|
||||||
id: string
|
id: string
|
||||||
options?: Record<string, unknown>
|
options?: Record<string, unknown>
|
||||||
|
is_default?: boolean
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export const Modules = {
|
|||||||
FILE: "file",
|
FILE: "file",
|
||||||
NOTIFICATION: "notification",
|
NOTIFICATION: "notification",
|
||||||
INDEX: "index",
|
INDEX: "index",
|
||||||
|
LOCKING: "locking",
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
export const MODULE_PACKAGE_NAMES = {
|
export const MODULE_PACKAGE_NAMES = {
|
||||||
@@ -52,6 +53,7 @@ export const MODULE_PACKAGE_NAMES = {
|
|||||||
[Modules.FILE]: "@medusajs/medusa/file",
|
[Modules.FILE]: "@medusajs/medusa/file",
|
||||||
[Modules.NOTIFICATION]: "@medusajs/medusa/notification",
|
[Modules.NOTIFICATION]: "@medusajs/medusa/notification",
|
||||||
[Modules.INDEX]: "@medusajs/medusa/index-module",
|
[Modules.INDEX]: "@medusajs/medusa/index-module",
|
||||||
|
[Modules.LOCKING]: "@medusajs/medusa/locking",
|
||||||
}
|
}
|
||||||
|
|
||||||
export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(
|
export const REVERSED_MODULE_PACKAGE_NAMES = Object.entries(
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
PerformedActions,
|
PerformedActions,
|
||||||
UpsertWithReplaceConfig,
|
UpsertWithReplaceConfig,
|
||||||
} from "@medusajs/types"
|
} from "@medusajs/types"
|
||||||
import type { EntitySchema, EntityClass } from "@mikro-orm/core"
|
import type { EntityClass, EntitySchema } from "@mikro-orm/core"
|
||||||
import {
|
import {
|
||||||
doNotForceTransaction,
|
doNotForceTransaction,
|
||||||
isDefined,
|
isDefined,
|
||||||
|
|||||||
@@ -83,6 +83,7 @@
|
|||||||
"@medusajs/index": "^0.0.1",
|
"@medusajs/index": "^0.0.1",
|
||||||
"@medusajs/inventory-next": "^0.0.3",
|
"@medusajs/inventory-next": "^0.0.3",
|
||||||
"@medusajs/link-modules": "^0.2.11",
|
"@medusajs/link-modules": "^0.2.11",
|
||||||
|
"@medusajs/locking": "^0.0.1",
|
||||||
"@medusajs/notification": "^0.1.2",
|
"@medusajs/notification": "^0.1.2",
|
||||||
"@medusajs/notification-local": "^0.0.1",
|
"@medusajs/notification-local": "^0.0.1",
|
||||||
"@medusajs/notification-sendgrid": "^0.0.1",
|
"@medusajs/notification-sendgrid": "^0.0.1",
|
||||||
|
|||||||
6
packages/medusa/src/modules/locking.ts
Normal file
6
packages/medusa/src/modules/locking.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import LockingModule from "@medusajs/locking"
|
||||||
|
|
||||||
|
export * from "@medusajs/locking"
|
||||||
|
|
||||||
|
export default LockingModule
|
||||||
|
export const discoveryPath = require.resolve("@medusajs/locking")
|
||||||
6
packages/modules/locking/.gitignore
vendored
Normal file
6
packages/modules/locking/.gitignore
vendored
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
/dist
|
||||||
|
node_modules
|
||||||
|
.DS_store
|
||||||
|
.env*
|
||||||
|
.env
|
||||||
|
*.sql
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
import { ILockingModule } from "@medusajs/framework/types"
|
||||||
|
import { Modules } from "@medusajs/framework/utils"
|
||||||
|
import { moduleIntegrationTestRunner } from "medusa-test-utils"
|
||||||
|
import { setTimeout } from "node:timers/promises"
|
||||||
|
|
||||||
|
jest.setTimeout(10000)
|
||||||
|
|
||||||
|
moduleIntegrationTestRunner<ILockingModule>({
|
||||||
|
moduleName: Modules.LOCKING,
|
||||||
|
testSuite: ({ service }) => {
|
||||||
|
describe("Locking Module Service", () => {
|
||||||
|
let stock = 5
|
||||||
|
function replenishStock() {
|
||||||
|
stock = 5
|
||||||
|
}
|
||||||
|
function hasStock() {
|
||||||
|
return stock > 0
|
||||||
|
}
|
||||||
|
async function reduceStock() {
|
||||||
|
await setTimeout(10)
|
||||||
|
stock--
|
||||||
|
}
|
||||||
|
async function buy() {
|
||||||
|
if (hasStock()) {
|
||||||
|
await reduceStock()
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
it("should execute functions respecting the key locked", async () => {
|
||||||
|
// 10 parallel calls to buy should oversell the stock
|
||||||
|
const prom: any[] = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
prom.push(buy())
|
||||||
|
}
|
||||||
|
await Promise.all(prom)
|
||||||
|
expect(stock).toBe(-5)
|
||||||
|
|
||||||
|
replenishStock()
|
||||||
|
|
||||||
|
// 10 parallel calls to buy with lock should not oversell the stock
|
||||||
|
const promWLock: any[] = []
|
||||||
|
for (let i = 0; i < 10; i++) {
|
||||||
|
promWLock.push(service.execute("item_1", buy))
|
||||||
|
}
|
||||||
|
await Promise.all(promWLock)
|
||||||
|
|
||||||
|
expect(stock).toBe(0)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should acquire lock and release it", async () => {
|
||||||
|
await service.acquire("key_name", {
|
||||||
|
ownerId: "user_id_123",
|
||||||
|
})
|
||||||
|
|
||||||
|
const userReleased = await service.release("key_name", {
|
||||||
|
ownerId: "user_id_456",
|
||||||
|
})
|
||||||
|
const anotherUserLock = service.acquire("key_name", {
|
||||||
|
ownerId: "user_id_456",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(userReleased).toBe(false)
|
||||||
|
await expect(anotherUserLock).rejects.toThrowError(
|
||||||
|
`"key_name" is already locked.`
|
||||||
|
)
|
||||||
|
|
||||||
|
const releasing = await service.release("key_name", {
|
||||||
|
ownerId: "user_id_123",
|
||||||
|
})
|
||||||
|
|
||||||
|
expect(releasing).toBe(true)
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should acquire lock and release it during parallel calls", async () => {
|
||||||
|
const keyToLock = "mySpecialKey"
|
||||||
|
const user_1 = {
|
||||||
|
ownerId: "user_id_456",
|
||||||
|
}
|
||||||
|
const user_2 = {
|
||||||
|
ownerId: "user_id_000",
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined()
|
||||||
|
|
||||||
|
expect(service.acquire(keyToLock, user_1)).resolves.toBeUndefined()
|
||||||
|
|
||||||
|
expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
|
||||||
|
`"${keyToLock}" is already locked.`
|
||||||
|
)
|
||||||
|
|
||||||
|
expect(service.acquire(keyToLock, user_2)).rejects.toThrowError(
|
||||||
|
`"${keyToLock}" is already locked.`
|
||||||
|
)
|
||||||
|
|
||||||
|
await service.acquire(keyToLock, user_1)
|
||||||
|
|
||||||
|
const releaseNotLocked = await service.release(keyToLock, {
|
||||||
|
ownerId: "user_id_000",
|
||||||
|
})
|
||||||
|
expect(releaseNotLocked).toBe(false)
|
||||||
|
|
||||||
|
const release = await service.release(keyToLock, user_1)
|
||||||
|
expect(release).toBe(true)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it("should release lock in case of failure", async () => {
|
||||||
|
const fn_1 = jest.fn(async () => {
|
||||||
|
throw new Error("Error")
|
||||||
|
})
|
||||||
|
const fn_2 = jest.fn(async () => {})
|
||||||
|
|
||||||
|
await service.execute("lock_key", fn_1).catch(() => {})
|
||||||
|
await service.execute("lock_key", fn_2).catch(() => {})
|
||||||
|
|
||||||
|
expect(fn_1).toBeCalledTimes(1)
|
||||||
|
expect(fn_2).toBeCalledTimes(1)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
})
|
||||||
10
packages/modules/locking/jest.config.js
Normal file
10
packages/modules/locking/jest.config.js
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
const defineJestConfig = require("../../../define_jest_config")
|
||||||
|
module.exports = defineJestConfig({
|
||||||
|
moduleNameMapper: {
|
||||||
|
"^@models": "<rootDir>/src/models",
|
||||||
|
"^@services": "<rootDir>/src/services",
|
||||||
|
"^@repositories": "<rootDir>/src/repositories",
|
||||||
|
"^@types": "<rootDir>/src/types",
|
||||||
|
"^@utils": "<rootDir>/src/utils",
|
||||||
|
},
|
||||||
|
})
|
||||||
6
packages/modules/locking/mikro-orm.config.dev.ts
Normal file
6
packages/modules/locking/mikro-orm.config.dev.ts
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
import { defineMikroOrmCliConfig, Modules } from "@medusajs/framework/utils"
|
||||||
|
import * as entities from "./src/models"
|
||||||
|
|
||||||
|
export default defineMikroOrmCliConfig(Modules.LOCKING, {
|
||||||
|
entities: Object.values(entities),
|
||||||
|
})
|
||||||
50
packages/modules/locking/package.json
Normal file
50
packages/modules/locking/package.json
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
{
|
||||||
|
"name": "@medusajs/locking",
|
||||||
|
"version": "0.0.1",
|
||||||
|
"description": "Locking Module for Medusa",
|
||||||
|
"main": "dist/index.js",
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/medusajs/medusa",
|
||||||
|
"directory": "packages/locking"
|
||||||
|
},
|
||||||
|
"publishConfig": {
|
||||||
|
"access": "public"
|
||||||
|
},
|
||||||
|
"author": "Medusa",
|
||||||
|
"license": "MIT",
|
||||||
|
"scripts": {
|
||||||
|
"watch": "tsc --build --watch",
|
||||||
|
"watch:test": "tsc --build tsconfig.spec.json --watch",
|
||||||
|
"resolve:aliases": "tsc --showConfig -p tsconfig.json > tsconfig.resolved.json && tsc-alias -p tsconfig.resolved.json && rimraf tsconfig.resolved.json",
|
||||||
|
"build": "rimraf dist && tsc --build && npm run resolve:aliases",
|
||||||
|
"test": "jest --passWithNoTests --runInBand --bail --forceExit -- src/",
|
||||||
|
"test:integration": "jest --runInBand --forceExit -- integration-tests/**/__tests__/**/*.spec.ts",
|
||||||
|
"migration:generate": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:generate",
|
||||||
|
"migration:initial": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create --initial -n InitialSetupMigration",
|
||||||
|
"migration:create": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:create",
|
||||||
|
"migration:up": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm migration:up",
|
||||||
|
"orm:cache:clear": " MIKRO_ORM_CLI=./mikro-orm.config.dev.ts medusa-mikro-orm cache:clear"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@medusajs/framework": "^0.0.1",
|
||||||
|
"@mikro-orm/cli": "5.9.7",
|
||||||
|
"@mikro-orm/core": "5.9.7",
|
||||||
|
"@mikro-orm/migrations": "5.9.7",
|
||||||
|
"@mikro-orm/postgresql": "5.9.7",
|
||||||
|
"@swc/core": "^1.7.28",
|
||||||
|
"@swc/jest": "^0.2.36",
|
||||||
|
"jest": "^29.7.0",
|
||||||
|
"medusa-test-utils": "^1.1.44",
|
||||||
|
"rimraf": "^3.0.2",
|
||||||
|
"tsc-alias": "^1.8.6",
|
||||||
|
"typescript": "^5.6.2"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"@medusajs/framework": "^0.0.1",
|
||||||
|
"@mikro-orm/core": "5.9.7",
|
||||||
|
"@mikro-orm/migrations": "5.9.7",
|
||||||
|
"@mikro-orm/postgresql": "5.9.7",
|
||||||
|
"awilix": "^8.0.1"
|
||||||
|
}
|
||||||
|
}
|
||||||
11
packages/modules/locking/src/index.ts
Normal file
11
packages/modules/locking/src/index.ts
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Module, Modules } from "@medusajs/framework/utils"
|
||||||
|
import { LockingModuleService } from "@services"
|
||||||
|
import loadProviders from "./loaders/providers"
|
||||||
|
|
||||||
|
export default Module(Modules.LOCKING, {
|
||||||
|
service: LockingModuleService,
|
||||||
|
loaders: [loadProviders],
|
||||||
|
})
|
||||||
|
|
||||||
|
// Module options types
|
||||||
|
export { LockingModuleOptions } from "./types"
|
||||||
85
packages/modules/locking/src/loaders/providers.ts
Normal file
85
packages/modules/locking/src/loaders/providers.ts
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import { moduleProviderLoader } from "@medusajs/framework/modules-sdk"
|
||||||
|
import {
|
||||||
|
LoaderOptions,
|
||||||
|
ModuleProvider,
|
||||||
|
ModulesSdkTypes,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
|
import { ContainerRegistrationKeys } from "@medusajs/framework/utils"
|
||||||
|
import { LockingProviderService } from "@services"
|
||||||
|
import {
|
||||||
|
LockingDefaultProvider,
|
||||||
|
LockingIdentifiersRegistrationName,
|
||||||
|
LockingProviderRegistrationPrefix,
|
||||||
|
} from "@types"
|
||||||
|
import { Lifetime, asFunction, asValue } from "awilix"
|
||||||
|
import { InMemoryLockingProvider } from "../providers/in-memory"
|
||||||
|
|
||||||
|
const registrationFn = async (klass, container, pluginOptions) => {
|
||||||
|
const key = LockingProviderService.getRegistrationIdentifier(klass)
|
||||||
|
|
||||||
|
container.register({
|
||||||
|
[LockingProviderRegistrationPrefix + key]: asFunction(
|
||||||
|
(cradle) => new klass(cradle, pluginOptions.options),
|
||||||
|
{
|
||||||
|
lifetime: klass.LIFE_TIME || Lifetime.SINGLETON,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
})
|
||||||
|
|
||||||
|
container.registerAdd(LockingIdentifiersRegistrationName, asValue(key))
|
||||||
|
}
|
||||||
|
|
||||||
|
export default async ({
|
||||||
|
container,
|
||||||
|
options,
|
||||||
|
}: LoaderOptions<
|
||||||
|
(
|
||||||
|
| ModulesSdkTypes.ModuleServiceInitializeOptions
|
||||||
|
| ModulesSdkTypes.ModuleServiceInitializeCustomDataLayerOptions
|
||||||
|
) & { providers: ModuleProvider[] }
|
||||||
|
>): Promise<void> => {
|
||||||
|
const logger = container.resolve(ContainerRegistrationKeys.LOGGER)
|
||||||
|
container.registerAdd(LockingIdentifiersRegistrationName, asValue(undefined))
|
||||||
|
|
||||||
|
// InMemoryLockingProvider - default provider
|
||||||
|
container.register({
|
||||||
|
[LockingProviderRegistrationPrefix + InMemoryLockingProvider.identifier]:
|
||||||
|
asFunction((cradle) => new InMemoryLockingProvider(), {
|
||||||
|
lifetime: Lifetime.SINGLETON,
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
container.registerAdd(
|
||||||
|
LockingIdentifiersRegistrationName,
|
||||||
|
asValue(InMemoryLockingProvider.identifier)
|
||||||
|
)
|
||||||
|
container.register(
|
||||||
|
LockingDefaultProvider,
|
||||||
|
asValue(InMemoryLockingProvider.identifier)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Load other providers
|
||||||
|
await moduleProviderLoader({
|
||||||
|
container,
|
||||||
|
providers: options?.providers || [],
|
||||||
|
registerServiceFn: registrationFn,
|
||||||
|
})
|
||||||
|
|
||||||
|
const isSingleProvider = options?.providers?.length === 1
|
||||||
|
let hasDefaultProvider = false
|
||||||
|
for (const provider of options?.providers || []) {
|
||||||
|
if (provider.is_default || isSingleProvider) {
|
||||||
|
if (provider.is_default) {
|
||||||
|
hasDefaultProvider = true
|
||||||
|
}
|
||||||
|
container.register(LockingDefaultProvider, asValue(provider.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasDefaultProvider) {
|
||||||
|
logger.warn(
|
||||||
|
`No default locking provider explicit defined. Using "${container.resolve(
|
||||||
|
LockingDefaultProvider
|
||||||
|
)}" as default.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
176
packages/modules/locking/src/providers/in-memory.ts
Normal file
176
packages/modules/locking/src/providers/in-memory.ts
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
import { ILockingProvider } from "@medusajs/framework/types"
|
||||||
|
import { isDefined } from "@medusajs/framework/utils"
|
||||||
|
|
||||||
|
type LockInfo = {
|
||||||
|
ownerId: string | null
|
||||||
|
expiration: number | null
|
||||||
|
currentPromise?: ResolvablePromise
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResolvablePromise = {
|
||||||
|
promise: Promise<any>
|
||||||
|
resolve: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export class InMemoryLockingProvider implements ILockingProvider {
|
||||||
|
static identifier = "in-memory"
|
||||||
|
|
||||||
|
private locks: Map<string, LockInfo> = new Map()
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
private getPromise(): ResolvablePromise {
|
||||||
|
let resolve: any
|
||||||
|
const pro = new Promise((ok) => {
|
||||||
|
resolve = ok
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
promise: pro,
|
||||||
|
resolve,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute<T>(
|
||||||
|
keys: string | string[],
|
||||||
|
job: () => Promise<T>,
|
||||||
|
args?: {
|
||||||
|
timeout?: number
|
||||||
|
}
|
||||||
|
): Promise<T> {
|
||||||
|
keys = Array.isArray(keys) ? keys : [keys]
|
||||||
|
|
||||||
|
const timeoutSeconds = args?.timeout ?? 5
|
||||||
|
|
||||||
|
const promises: Promise<any>[] = []
|
||||||
|
if (timeoutSeconds > 0) {
|
||||||
|
promises.push(this.getTimeout(timeoutSeconds))
|
||||||
|
}
|
||||||
|
|
||||||
|
promises.push(
|
||||||
|
this.acquire(keys, {
|
||||||
|
awaitQueue: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
await Promise.race(promises).catch(async (err) => {
|
||||||
|
await this.release(keys)
|
||||||
|
})
|
||||||
|
|
||||||
|
try {
|
||||||
|
return await job()
|
||||||
|
} finally {
|
||||||
|
await this.release(keys)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquire(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
expire?: number
|
||||||
|
awaitQueue?: boolean
|
||||||
|
}
|
||||||
|
): Promise<void> {
|
||||||
|
keys = Array.isArray(keys) ? keys : [keys]
|
||||||
|
const { ownerId, expire } = args ?? {}
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const lock = this.locks.get(key)
|
||||||
|
const now = Date.now()
|
||||||
|
|
||||||
|
if (!lock) {
|
||||||
|
this.locks.set(key, {
|
||||||
|
ownerId: ownerId ?? null,
|
||||||
|
expiration: expire ? now + expire * 1000 : null,
|
||||||
|
currentPromise: this.getPromise(),
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lock.expiration && lock.expiration <= now) {
|
||||||
|
lock.currentPromise?.resolve?.()
|
||||||
|
this.locks.set(key, {
|
||||||
|
ownerId: ownerId ?? null,
|
||||||
|
expiration: expire ? now + expire * 1000 : null,
|
||||||
|
currentPromise: this.getPromise(),
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lock.ownerId === ownerId) {
|
||||||
|
if (expire) {
|
||||||
|
lock.expiration = now + expire * 1000
|
||||||
|
this.locks.set(key, lock)
|
||||||
|
}
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lock.currentPromise && args?.awaitQueue) {
|
||||||
|
await lock.currentPromise.promise
|
||||||
|
return this.acquire(keys, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(`"${key}" is already locked.`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async release(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
}
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { ownerId } = args ?? {}
|
||||||
|
keys = Array.isArray(keys) ? keys : [keys]
|
||||||
|
|
||||||
|
let success = true
|
||||||
|
|
||||||
|
for (const key of keys) {
|
||||||
|
const lock = this.locks.get(key)
|
||||||
|
if (!lock) {
|
||||||
|
success = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isDefined(ownerId) && lock.ownerId !== ownerId) {
|
||||||
|
success = false
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
lock.currentPromise?.resolve?.()
|
||||||
|
this.locks.delete(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
return success
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseAll(args?: { ownerId?: string | null }): Promise<void> {
|
||||||
|
const { ownerId } = args ?? {}
|
||||||
|
|
||||||
|
if (!isDefined(ownerId)) {
|
||||||
|
for (const [key, lock] of this.locks.entries()) {
|
||||||
|
lock.currentPromise?.resolve?.()
|
||||||
|
this.locks.delete(key)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const [key, lock] of this.locks.entries()) {
|
||||||
|
if (lock.ownerId === ownerId) {
|
||||||
|
lock.currentPromise?.resolve?.()
|
||||||
|
this.locks.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async getTimeout(seconds: number): Promise<void> {
|
||||||
|
return new Promise((_, reject) => {
|
||||||
|
setTimeout(() => {
|
||||||
|
reject(new Error("Timed-out acquiring lock."))
|
||||||
|
}, seconds * 1000)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
2
packages/modules/locking/src/services/index.ts
Normal file
2
packages/modules/locking/src/services/index.ts
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
export { default as LockingModuleService } from "./locking-module"
|
||||||
|
export { default as LockingProviderService } from "./locking-provider"
|
||||||
90
packages/modules/locking/src/services/locking-module.ts
Normal file
90
packages/modules/locking/src/services/locking-module.ts
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
import {
|
||||||
|
Context,
|
||||||
|
ILockingModule,
|
||||||
|
InternalModuleDeclaration,
|
||||||
|
} from "@medusajs/types"
|
||||||
|
import { EntityManager } from "@mikro-orm/core"
|
||||||
|
import { LockingDefaultProvider } from "@types"
|
||||||
|
import LockingProviderService from "./locking-provider"
|
||||||
|
|
||||||
|
type InjectedDependencies = {
|
||||||
|
manager: EntityManager
|
||||||
|
lockingProviderService: LockingProviderService
|
||||||
|
[LockingDefaultProvider]: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LockingModuleService implements ILockingModule {
|
||||||
|
protected manager: EntityManager
|
||||||
|
protected providerService_: LockingProviderService
|
||||||
|
protected defaultProviderId: string
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
container: InjectedDependencies,
|
||||||
|
protected readonly moduleDeclaration: InternalModuleDeclaration
|
||||||
|
) {
|
||||||
|
this.manager = container.manager
|
||||||
|
this.providerService_ = container.lockingProviderService
|
||||||
|
this.defaultProviderId = container[LockingDefaultProvider]
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute<T>(
|
||||||
|
keys: string | string[],
|
||||||
|
job: () => Promise<T>,
|
||||||
|
args?: {
|
||||||
|
timeout?: number
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext: Context = {}
|
||||||
|
): Promise<T> {
|
||||||
|
const providerId = args?.provider ?? this.defaultProviderId
|
||||||
|
const provider =
|
||||||
|
this.providerService_.retrieveProviderRegistration(providerId)
|
||||||
|
|
||||||
|
return provider.execute(keys, job, args, sharedContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
async acquire(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
expire?: number
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext: Context = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const providerId = args?.provider ?? this.defaultProviderId
|
||||||
|
const provider =
|
||||||
|
this.providerService_.retrieveProviderRegistration(providerId)
|
||||||
|
|
||||||
|
await provider.acquire(keys, args, sharedContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
async release(
|
||||||
|
keys: string | string[],
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext: Context = {}
|
||||||
|
): Promise<boolean> {
|
||||||
|
const providerId = args?.provider ?? this.defaultProviderId
|
||||||
|
const provider =
|
||||||
|
this.providerService_.retrieveProviderRegistration(providerId)
|
||||||
|
|
||||||
|
return await provider.release(keys, args, sharedContext)
|
||||||
|
}
|
||||||
|
|
||||||
|
async releaseAll(
|
||||||
|
args?: {
|
||||||
|
ownerId?: string | null
|
||||||
|
provider?: string
|
||||||
|
},
|
||||||
|
sharedContext: Context = {}
|
||||||
|
): Promise<void> {
|
||||||
|
const providerId = args?.provider ?? this.defaultProviderId
|
||||||
|
const provider =
|
||||||
|
this.providerService_.retrieveProviderRegistration(providerId)
|
||||||
|
|
||||||
|
return await provider.releaseAll(args, sharedContext)
|
||||||
|
}
|
||||||
|
}
|
||||||
40
packages/modules/locking/src/services/locking-provider.ts
Normal file
40
packages/modules/locking/src/services/locking-provider.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { Constructor, ILockingProvider } from "@medusajs/framework/types"
|
||||||
|
import { MedusaError } from "@medusajs/framework/utils"
|
||||||
|
import { LockingProviderRegistrationPrefix } from "../types"
|
||||||
|
|
||||||
|
type InjectedDependencies = {
|
||||||
|
[key: `lp_${string}`]: ILockingProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
export default class LockingProviderService {
|
||||||
|
protected __container__: InjectedDependencies
|
||||||
|
|
||||||
|
constructor(container: InjectedDependencies) {
|
||||||
|
this.__container__ = container
|
||||||
|
}
|
||||||
|
|
||||||
|
static getRegistrationIdentifier(
|
||||||
|
providerClass: Constructor<ILockingProvider>
|
||||||
|
) {
|
||||||
|
if (!(providerClass as any).identifier) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.INVALID_ARGUMENT,
|
||||||
|
`Trying to register a locking provider without an identifier.`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return `${(providerClass as any).identifier}`
|
||||||
|
}
|
||||||
|
|
||||||
|
public retrieveProviderRegistration(providerId: string): ILockingProvider {
|
||||||
|
try {
|
||||||
|
return this.__container__[
|
||||||
|
`${LockingProviderRegistrationPrefix}${providerId}`
|
||||||
|
]
|
||||||
|
} catch (err) {
|
||||||
|
throw new MedusaError(
|
||||||
|
MedusaError.Types.NOT_FOUND,
|
||||||
|
`Could not find a locking provider with id: ${providerId}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/modules/locking/src/types/index.ts
Normal file
33
packages/modules/locking/src/types/index.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
ModuleProviderExports,
|
||||||
|
ModuleServiceInitializeOptions,
|
||||||
|
} from "@medusajs/framework/types"
|
||||||
|
|
||||||
|
export const LockingDefaultProvider = "default_provider"
|
||||||
|
export const LockingIdentifiersRegistrationName = "locking_providers_identifier"
|
||||||
|
|
||||||
|
export const LockingProviderRegistrationPrefix = "lp_"
|
||||||
|
|
||||||
|
export type LockingModuleOptions = Partial<ModuleServiceInitializeOptions> & {
|
||||||
|
/**
|
||||||
|
* Providers to be registered
|
||||||
|
*/
|
||||||
|
providers?: {
|
||||||
|
/**
|
||||||
|
* The module provider to be registered
|
||||||
|
*/
|
||||||
|
resolve: string | ModuleProviderExports
|
||||||
|
/**
|
||||||
|
* If the provider is the default
|
||||||
|
*/
|
||||||
|
is_default?: boolean
|
||||||
|
/**
|
||||||
|
* The id of the provider
|
||||||
|
*/
|
||||||
|
id: string
|
||||||
|
/**
|
||||||
|
* key value pair of the configuration to be passed to the provider constructor
|
||||||
|
*/
|
||||||
|
options?: Record<string, unknown>
|
||||||
|
}[]
|
||||||
|
}
|
||||||
12
packages/modules/locking/tsconfig.json
Normal file
12
packages/modules/locking/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"extends": "../../../_tsconfig.base.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@models": ["./src/models"],
|
||||||
|
"@services": ["./src/services"],
|
||||||
|
"@repositories": ["./src/repositories"],
|
||||||
|
"@types": ["./src/types"],
|
||||||
|
"@utils": ["./src/utils"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
26
yarn.lock
26
yarn.lock
@@ -5652,6 +5652,31 @@ __metadata:
|
|||||||
languageName: unknown
|
languageName: unknown
|
||||||
linkType: soft
|
linkType: soft
|
||||||
|
|
||||||
|
"@medusajs/locking@^0.0.1, @medusajs/locking@workspace:packages/modules/locking":
|
||||||
|
version: 0.0.0-use.local
|
||||||
|
resolution: "@medusajs/locking@workspace:packages/modules/locking"
|
||||||
|
dependencies:
|
||||||
|
"@medusajs/framework": ^0.0.1
|
||||||
|
"@mikro-orm/cli": 5.9.7
|
||||||
|
"@mikro-orm/core": 5.9.7
|
||||||
|
"@mikro-orm/migrations": 5.9.7
|
||||||
|
"@mikro-orm/postgresql": 5.9.7
|
||||||
|
"@swc/core": ^1.7.28
|
||||||
|
"@swc/jest": ^0.2.36
|
||||||
|
jest: ^29.7.0
|
||||||
|
medusa-test-utils: ^1.1.44
|
||||||
|
rimraf: ^3.0.2
|
||||||
|
tsc-alias: ^1.8.6
|
||||||
|
typescript: ^5.6.2
|
||||||
|
peerDependencies:
|
||||||
|
"@medusajs/framework": ^0.0.1
|
||||||
|
"@mikro-orm/core": 5.9.7
|
||||||
|
"@mikro-orm/migrations": 5.9.7
|
||||||
|
"@mikro-orm/postgresql": 5.9.7
|
||||||
|
awilix: ^8.0.1
|
||||||
|
languageName: unknown
|
||||||
|
linkType: soft
|
||||||
|
|
||||||
"@medusajs/medusa-cli@^1.3.22, @medusajs/medusa-cli@workspace:packages/cli/medusa-cli":
|
"@medusajs/medusa-cli@^1.3.22, @medusajs/medusa-cli@workspace:packages/cli/medusa-cli":
|
||||||
version: 0.0.0-use.local
|
version: 0.0.0-use.local
|
||||||
resolution: "@medusajs/medusa-cli@workspace:packages/cli/medusa-cli"
|
resolution: "@medusajs/medusa-cli@workspace:packages/cli/medusa-cli"
|
||||||
@@ -5761,6 +5786,7 @@ __metadata:
|
|||||||
"@medusajs/index": ^0.0.1
|
"@medusajs/index": ^0.0.1
|
||||||
"@medusajs/inventory-next": ^0.0.3
|
"@medusajs/inventory-next": ^0.0.3
|
||||||
"@medusajs/link-modules": ^0.2.11
|
"@medusajs/link-modules": ^0.2.11
|
||||||
|
"@medusajs/locking": ^0.0.1
|
||||||
"@medusajs/notification": ^0.1.2
|
"@medusajs/notification": ^0.1.2
|
||||||
"@medusajs/notification-local": ^0.0.1
|
"@medusajs/notification-local": ^0.0.1
|
||||||
"@medusajs/notification-sendgrid": ^0.0.1
|
"@medusajs/notification-sendgrid": ^0.0.1
|
||||||
|
|||||||
Reference in New Issue
Block a user