diff --git a/.changeset/hip-teachers-do.md b/.changeset/hip-teachers-do.md new file mode 100644 index 0000000..d8eff27 --- /dev/null +++ b/.changeset/hip-teachers-do.md @@ -0,0 +1,6 @@ +--- +"@totalsoft/correlation": patch +"@totalsoft/multitenancy-core": patch +--- + +Fix: Ensure correlation ID and tenant context are kept separate between concurrent calls. diff --git a/packages/correlation/__tests__/correlation.test.ts b/packages/correlation/__tests__/correlation.test.ts index db8fca3..922eb61 100644 --- a/packages/correlation/__tests__/correlation.test.ts +++ b/packages/correlation/__tests__/correlation.test.ts @@ -46,4 +46,26 @@ describe('correlation tests:', () => { //assert expect(correlationId).toHaveLength(36) }) + + it('does not mix correlation ids between concurrent requests', async () => { + //arrange + let correlationId1: string | undefined + let correlationId2: string | undefined + + //act + await Promise.all([ + correlationManager.useCorrelationId('request-1', async () => { + await new Promise(resolve => setTimeout(resolve, 10)) + correlationId1 = correlationManager.getCorrelationId() + }), + correlationManager.useCorrelationId('request-2', async () => { + await new Promise(resolve => setTimeout(resolve, 5)) + correlationId2 = correlationManager.getCorrelationId() + }) + ]) + + //assert + expect(correlationId1).toBe('request-1') + expect(correlationId2).toBe('request-2') + }) }) diff --git a/packages/correlation/src/correlationManager.ts b/packages/correlation/src/correlationManager.ts index 44f6798..a5c9210 100644 --- a/packages/correlation/src/correlationManager.ts +++ b/packages/correlation/src/correlationManager.ts @@ -4,18 +4,15 @@ import { AsyncLocalStorage } from 'async_hooks' import { v4 } from 'uuid' -const asyncLocalStorage = new AsyncLocalStorage>() -const store = new Map() -const correlationIdKey = 'correlationId' +const asyncLocalStorage = new AsyncLocalStorage() const getCorrelationId = () => { - const correlationIdStore = asyncLocalStorage.getStore() - return correlationIdStore?.get(correlationIdKey) + return asyncLocalStorage.getStore() } async function useCorrelationId(correlationId: string | null, next: () => Promise) { - return asyncLocalStorage.run(store, async () => { - store.set(correlationIdKey, correlationId || v4()) + const correlationIdToUse = correlationId || v4() + return asyncLocalStorage.run(correlationIdToUse, async () => { return await next() }) } diff --git a/packages/multitenancy-core/__tests__/tenant-context-accessor.test.ts b/packages/multitenancy-core/__tests__/tenant-context-accessor.test.ts new file mode 100644 index 0000000..965c60b --- /dev/null +++ b/packages/multitenancy-core/__tests__/tenant-context-accessor.test.ts @@ -0,0 +1,71 @@ +// Copyright (c) TotalSoft. +// This source code is licensed under the MIT license. + +import { tenantContextAccessor } from '../src' + +describe('tenant context accessor tests:', () => { + it('passes tenant context in async flow', async () => { + //arrange + const tenant = { id: 'tenant1', code: 'tenant1-code', enabled: true } + let tenantContext + async function inner() { + tenantContext = tenantContextAccessor.getTenantContext() + } + + //act + await tenantContextAccessor.useTenantContext({ tenant }, async () => { + await inner() + }) + + //assert + expect(tenantContext).toHaveProperty('tenant', tenant) + }) + + it('returns empty context if tenant context not set', async () => { + //arrange + let tenantContext + async function inner() { + tenantContext = tenantContextAccessor.getTenantContext() + } + + //act + await inner() + + //assert + expect(tenantContext).not.toBe(undefined) + expect(tenantContext).not.toHaveProperty('tenant') + }) + + it('does not share tenant context between concurrent requests', async () => { + //arrange + const tenant1 = { id: 'tenant1', code: 'tenant1-code', enabled: true } + const tenant2 = { id: 'tenant2', code: 'tenant2-code', enabled: true } + let capturedContext1: ReturnType = {} as ReturnType + let capturedContext2: ReturnType = {} as ReturnType + + const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms)) + + async function request1() { + await tenantContextAccessor.useTenantContext({ tenant: tenant1 }, async () => { + await delay(10) + capturedContext1 = tenantContextAccessor.getTenantContext() + await delay(10) + }) + } + + async function request2() { + await tenantContextAccessor.useTenantContext({ tenant: tenant2 }, async () => { + await delay(5) + capturedContext2 = tenantContextAccessor.getTenantContext() + await delay(15) + }) + } + + //act + await Promise.all([request1(), request2()]) + + //assert + expect(capturedContext1).toHaveProperty('tenant', tenant1) + expect(capturedContext2).toHaveProperty('tenant', tenant2) + }) +}) diff --git a/packages/multitenancy-core/__tests__/tenant-context-accessor.text.ts b/packages/multitenancy-core/__tests__/tenant-context-accessor.text.ts deleted file mode 100644 index fb404dc..0000000 --- a/packages/multitenancy-core/__tests__/tenant-context-accessor.text.ts +++ /dev/null @@ -1,38 +0,0 @@ -// Copyright (c) TotalSoft. -// This source code is licensed under the MIT license. - -import { tenantContextAccessor } from '../src' - -describe('tenant context accessor tests:', () => { - it('passes tenant context in async flow', async () => { - //arrange - const tenant = { id: 'tenant1', code: 'tenant1-code', enabled: true } - let tenantContext - async function inner() { - tenantContext = tenantContextAccessor.getTenantContext() - } - - //act - tenantContextAccessor.useTenantContext({ tenant }, async () => { - await inner() - }) - - //assert - expect(tenantContext).toHaveProperty('tenant', tenant) - }) - - it('returns empty context if tenant context not set', async () => { - //arrange - let tenantContext - async function inner() { - tenantContext = tenantContextAccessor.getTenantContext() - } - - //act - await inner() - - //assert - expect(tenantContext).not.toBe(undefined) - expect(tenantContext).not.toHaveProperty('tenant') - }) -}) diff --git a/packages/multitenancy-core/src/tenantContextAccessor.ts b/packages/multitenancy-core/src/tenantContextAccessor.ts index f133ef8..841d61e 100644 --- a/packages/multitenancy-core/src/tenantContextAccessor.ts +++ b/packages/multitenancy-core/src/tenantContextAccessor.ts @@ -4,16 +4,15 @@ import { AsyncLocalStorage } from 'async_hooks' import { TenantContext } from './types' -const asyncLocalStorage = new AsyncLocalStorage>() -const store = new Map() +const asyncLocalStorage = new AsyncLocalStorage() /** * Access the current tenant context in scope * @returns - the tenant context */ const getTenantContext = (): TenantContext => { - const tenantStore = asyncLocalStorage.getStore() - return tenantStore?.get('tenantContext') ?? {} + const tenantContext = asyncLocalStorage.getStore() + return tenantContext ?? {} } /** @@ -23,8 +22,7 @@ const getTenantContext = (): TenantContext => { * @returns the result of the next function */ async function useTenantContext(tenantContext: TenantContext, next: () => Promise) { - return asyncLocalStorage.run(store, async () => { - store.set('tenantContext', tenantContext) + return asyncLocalStorage.run(tenantContext, async () => { return await next() }) } diff --git a/packages/multitenancy-core/src/types.ts b/packages/multitenancy-core/src/types.ts index 60b1b40..20c008c 100644 --- a/packages/multitenancy-core/src/types.ts +++ b/packages/multitenancy-core/src/types.ts @@ -31,5 +31,5 @@ export interface TenantSection { } export interface TenantContext { - tenant: Tenant; + tenant?: Tenant }