Merged PR 1881: Stateful Remi Button

#5203

Related work items: #5203
This commit is contained in:
Lorenz Hilpert
2025-07-14 11:57:03 +00:00
committed by Nino Righi
parent 5f74c6ddf8
commit 40c9d51dfc
24 changed files with 1418 additions and 604 deletions

View File

@@ -9,6 +9,7 @@ describe('InFlight Decorators', () => {
afterEach(() => {
vi.restoreAllMocks();
vi.clearAllTimers();
vi.useRealTimers();
});
describe('InFlight', () => {
@@ -75,17 +76,27 @@ describe('InFlight Decorators', () => {
const promise1 = service.fetchWithError();
const promise2 = service.fetchWithError();
// Handle the promises immediately to avoid unhandled rejections
const resultsPromise = Promise.allSettled([promise1, promise2]);
await vi.runAllTimersAsync();
// Both should reject with the same error
await expect(promise1).rejects.toThrow('Test error');
await expect(promise2).rejects.toThrow('Test error');
const results = await resultsPromise;
expect(results[0].status).toBe('rejected');
expect(results[1].status).toBe('rejected');
expect((results[0] as PromiseRejectedResult).reason.message).toBe('Test error');
expect((results[1] as PromiseRejectedResult).reason.message).toBe('Test error');
expect(service.callCount).toBe(1);
// Should allow new call after error
const promise3 = service.fetchWithError();
const promise3Result = Promise.allSettled([promise3]);
await vi.runAllTimersAsync();
await expect(promise3).rejects.toThrow('Test error');
const [result3] = await promise3Result;
expect(result3.status).toBe('rejected');
expect((result3 as PromiseRejectedResult).reason.message).toBe('Test error');
expect(service.callCount).toBe(2);
});
@@ -307,14 +318,20 @@ describe('InFlight Decorators', () => {
// First call that errors
const promise1 = service.fetchWithError();
const promise1Result = Promise.allSettled([promise1]);
await vi.runAllTimersAsync();
await expect(promise1).rejects.toThrow('API Error');
const result1 = await promise1Result;
expect(result1[0].status).toBe('rejected');
expect((result1[0] as PromiseRejectedResult).reason.message).toBe('API Error');
expect(service.callCount).toBe(1);
// Second call should not use cache (errors aren't cached)
const promise2 = service.fetchWithError();
const promise2Result = Promise.allSettled([promise2]);
await vi.runAllTimersAsync();
await expect(promise2).rejects.toThrow('API Error');
const result2 = await promise2Result;
expect(result2[0].status).toBe('rejected');
expect((result2[0] as PromiseRejectedResult).reason.message).toBe('API Error');
expect(service.callCount).toBe(2);
});
});

View File

@@ -20,8 +20,8 @@ export function InFlight<
const inFlightMap = new WeakMap<object, Promise<any>>();
return function (
target: any,
propertyKey: string | symbol,
_target: any,
_propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
@@ -39,15 +39,9 @@ export function InFlight<
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
.finally(() => {
// Always clean up in-flight request
inFlightMap.delete(this);
return result;
})
.catch((error: any) => {
// Clean up after error
inFlightMap.delete(this);
throw error;
});
inFlightMap.set(this, promise);
@@ -92,8 +86,8 @@ export function InFlightWithKey<T extends (...args: any[]) => Promise<any>>(
const inFlightMap = new WeakMap<object, Map<string, Promise<any>>>();
return function (
target: any,
propertyKey: string | symbol,
_target: any,
_propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
@@ -106,7 +100,7 @@ export function InFlightWithKey<T extends (...args: any[]) => Promise<any>>(
if (!inFlightMap.has(this)) {
inFlightMap.set(this, new Map());
}
const instanceMap = inFlightMap.get(this)!;
const instanceMap = inFlightMap.get(this) as Map<string, Promise<any>>;
// Generate cache key
const key = options.keyGenerator
@@ -122,15 +116,9 @@ export function InFlightWithKey<T extends (...args: any[]) => Promise<any>>(
// Create new request and store it
const promise = originalMethod
.apply(this, args)
.then((result: any) => {
// Clean up after successful completion
.finally(() => {
// Always clean up in-flight request
instanceMap.delete(key);
return result;
})
.catch((error: any) => {
// Clean up after error
instanceMap.delete(key);
throw error;
});
instanceMap.set(key, promise);
@@ -183,8 +171,8 @@ export function InFlightWithCache<T extends (...args: any[]) => Promise<any>>(
>();
return function (
target: any,
propertyKey: string | symbol,
_target: any,
_propertyKey: string | symbol,
descriptor: PropertyDescriptor,
): PropertyDescriptor {
const originalMethod = descriptor.value;
@@ -198,8 +186,8 @@ export function InFlightWithCache<T extends (...args: any[]) => Promise<any>>(
inFlightMap.set(this, new Map());
cacheMap.set(this, new Map());
}
const instanceInFlight = inFlightMap.get(this)!;
const instanceCache = cacheMap.get(this)!;
const instanceInFlight = inFlightMap.get(this) as Map<string, Promise<any>>;
const instanceCache = cacheMap.get(this) as Map<string, { result: any; expiry: number }>;
// Generate cache key
const key = options.keyGenerator