mirror of
https://github.com/Unleash/unleash.git
synced 2025-07-26 13:48:33 +02:00
feat: add transaction context store
This commit is contained in:
parent
4471b3ff00
commit
2570fe5e97
@ -1,5 +1,6 @@
|
||||
import type { Knex } from 'knex';
|
||||
import type { IUnleashConfig } from '../types/index.ts';
|
||||
import { transactionContext } from '../util/transactionContext.js';
|
||||
|
||||
export type KnexTransaction = Knex.Transaction;
|
||||
|
||||
@ -75,13 +76,14 @@ export function withTransactional<S>(
|
||||
): WithTransactional<S> {
|
||||
const service = serviceFactory(db) as WithTransactional<S>;
|
||||
|
||||
service.transactional = async <R>(fn: (service: S) => R) =>
|
||||
// Maybe: inTransaction(db, async (trx: Knex.Transaction) => fn(serviceFactory(trx)));
|
||||
// this assumes that the caller didn't start a transaction already and opens a new one.
|
||||
db.transaction(async (trx: Knex.Transaction) => {
|
||||
const transactionalService = serviceFactory(trx);
|
||||
return fn(transactionalService);
|
||||
service.transactional = async <R>(fn: (service: S) => R): Promise<R> => {
|
||||
return db.transaction(async (trx) => {
|
||||
return transactionContext.run(async () => {
|
||||
const transactionalService = serviceFactory(trx);
|
||||
return fn(transactionalService);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return service;
|
||||
}
|
||||
|
191
src/lib/util/transactionContext.test.ts
Normal file
191
src/lib/util/transactionContext.test.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { transactionContext } from './transactionContext.js';
|
||||
|
||||
describe('transactionContext', () => {
|
||||
describe('run', () => {
|
||||
it('should execute callback with transaction context', async () => {
|
||||
const result = await transactionContext.run(async () => {
|
||||
return 'callback-result';
|
||||
});
|
||||
|
||||
expect(result).toBe('callback-result');
|
||||
});
|
||||
|
||||
it('should make transaction context available inside the callback', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
expect(transactionContext.getOperationId()).toBeDefined();
|
||||
expect(typeof transactionContext.getOperationId()).toBe(
|
||||
'number',
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should generate unique numeric transaction IDs', async () => {
|
||||
const ids: (string | number | undefined)[] = [];
|
||||
|
||||
await transactionContext.run(async () => {
|
||||
ids.push(transactionContext.getOperationId());
|
||||
});
|
||||
|
||||
await transactionContext.run(async () => {
|
||||
ids.push(transactionContext.getOperationId());
|
||||
});
|
||||
|
||||
expect(ids).toHaveLength(2);
|
||||
expect(ids[0]).not.toBe(ids[1]);
|
||||
expect(typeof ids[0]).toBe('number');
|
||||
expect(typeof ids[1]).toBe('number');
|
||||
});
|
||||
|
||||
it('should handle rejected promises', async () => {
|
||||
const error = new Error('Test error');
|
||||
|
||||
await expect(
|
||||
transactionContext.run(async () => {
|
||||
throw error;
|
||||
}),
|
||||
).rejects.toThrow('Test error');
|
||||
});
|
||||
|
||||
it('should handle nested transaction contexts', async () => {
|
||||
let outerOperationId: string | number | undefined;
|
||||
let innerOperationId: string | number | undefined;
|
||||
|
||||
await transactionContext.run(async () => {
|
||||
outerOperationId = transactionContext.getOperationId();
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
|
||||
await transactionContext.run(async () => {
|
||||
innerOperationId = transactionContext.getOperationId();
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
});
|
||||
|
||||
expect(transactionContext.getOperationId()).toBe(
|
||||
outerOperationId,
|
||||
);
|
||||
});
|
||||
|
||||
expect(outerOperationId).toBeDefined();
|
||||
expect(innerOperationId).toBeDefined();
|
||||
expect(outerOperationId).not.toBe(innerOperationId);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperation', () => {
|
||||
it('should return undefined when called outside of transaction context', () => {
|
||||
expect(transactionContext.getOperation()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return the operation context when called inside context', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
const operation = transactionContext.getOperation();
|
||||
expect(operation).toBeDefined();
|
||||
expect(operation?.type).toBe('transaction');
|
||||
expect(operation?.id).toBeDefined();
|
||||
expect(typeof operation?.id).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperationType', () => {
|
||||
it('should return undefined when called outside of transaction context', () => {
|
||||
expect(transactionContext.getOperationType()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return "transaction" when called inside context', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOperationId', () => {
|
||||
it('should return undefined when called outside of transaction context', () => {
|
||||
expect(transactionContext.getOperationId()).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should return numeric ID when called inside context', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
const id = transactionContext.getOperationId();
|
||||
expect(id).toBeDefined();
|
||||
expect(typeof id).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('setOperation', () => {
|
||||
it('should throw error when called outside of transaction context', () => {
|
||||
expect(() => {
|
||||
transactionContext.setOperation({
|
||||
type: 'change-request',
|
||||
id: 123,
|
||||
});
|
||||
}).toThrow(
|
||||
'No active transaction context found when setting operation',
|
||||
);
|
||||
});
|
||||
|
||||
it('should set operation context in active context', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
|
||||
transactionContext.setOperation({
|
||||
type: 'change-request',
|
||||
id: 456,
|
||||
});
|
||||
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'change-request',
|
||||
);
|
||||
expect(transactionContext.getOperationId()).toBe(456);
|
||||
});
|
||||
});
|
||||
|
||||
it('should update existing operation context', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'transaction',
|
||||
);
|
||||
const originalId = transactionContext.getOperationId();
|
||||
|
||||
transactionContext.setOperation({
|
||||
type: 'change-request',
|
||||
id: 789,
|
||||
});
|
||||
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'change-request',
|
||||
);
|
||||
expect(transactionContext.getOperationId()).toBe(789);
|
||||
expect(transactionContext.getOperationId()).not.toBe(
|
||||
originalId,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('should not affect transaction context after callback completes', async () => {
|
||||
await transactionContext.run(async () => {
|
||||
transactionContext.setOperation({
|
||||
type: 'change-request',
|
||||
id: 999,
|
||||
});
|
||||
expect(transactionContext.getOperationType()).toBe(
|
||||
'change-request',
|
||||
);
|
||||
expect(transactionContext.getOperationId()).toBe(999);
|
||||
});
|
||||
|
||||
expect(transactionContext.getOperation()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
49
src/lib/util/transactionContext.ts
Normal file
49
src/lib/util/transactionContext.ts
Normal file
@ -0,0 +1,49 @@
|
||||
import { AsyncLocalStorage } from 'async_hooks';
|
||||
|
||||
export interface OperationContext {
|
||||
type: 'change-request' | 'transaction';
|
||||
id: number;
|
||||
}
|
||||
|
||||
// Generate a numeric transaction ID based on timestamp + random component
|
||||
function generateNumericTransactionId(): number {
|
||||
const timestamp = Date.now(); // 13 digits
|
||||
const random = Math.floor(Math.random() * 1000); // 3 digits max
|
||||
return timestamp * 1000 + random; // Ensures uniqueness
|
||||
}
|
||||
|
||||
const storage = new AsyncLocalStorage<OperationContext>();
|
||||
|
||||
export const transactionContext = {
|
||||
run<T>(callback: () => Promise<T>): Promise<T> {
|
||||
const data: OperationContext = {
|
||||
type: 'transaction',
|
||||
id: generateNumericTransactionId(),
|
||||
};
|
||||
return storage.run(data, callback) as Promise<T>;
|
||||
},
|
||||
|
||||
getOperation(): OperationContext | undefined {
|
||||
return storage.getStore();
|
||||
},
|
||||
|
||||
getOperationType(): OperationContext['type'] | undefined {
|
||||
return storage.getStore()?.type;
|
||||
},
|
||||
|
||||
getOperationId(): string | number | undefined {
|
||||
return storage.getStore()?.id;
|
||||
},
|
||||
|
||||
setOperation(operation: OperationContext): void {
|
||||
const store = storage.getStore();
|
||||
if (store) {
|
||||
store.id = operation.id;
|
||||
store.type = operation.type;
|
||||
} else {
|
||||
throw new Error(
|
||||
'No active transaction context found when setting operation',
|
||||
);
|
||||
}
|
||||
},
|
||||
};
|
Loading…
Reference in New Issue
Block a user