Skip to content

Commit 65165ca

Browse files
authored
PromiseCache: re-use lazy concept (#138)
* lazy: correct error type - unknown * PromiesCache: correct types null -> undefined * promiseCache: getLazy as replacement to getDeferred * tests refactoring + fake timers used * improved tests coverage * bumped version
1 parent 9aba1d3 commit 65165ca

21 files changed

Lines changed: 1182 additions & 746 deletions

File tree

package-lock.json

Lines changed: 35 additions & 34 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

packages/common-mobx/src/lazy/__tests__/lazy.test.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -80,8 +80,9 @@ describe('LazyPromise', () => {
8080
expect(l.value).toBeUndefined();
8181
await l.promise;
8282

83-
expect(errorListener).toHaveBeenCalledWith('Test error');
84-
expect(l.error).toBe('Test error');
83+
expect(errorListener).toHaveBeenCalledWith(expect.any(Error));
84+
expect(l.error).toBeInstanceOf(Error);
85+
expect(l.errorMessage).toBe('Test error');
8586

8687
errorListener.mockClear();
8788

@@ -118,8 +119,9 @@ describe('LazyPromise', () => {
118119
expect(errorListener).not.toHaveBeenCalled();
119120

120121
await l.refresh();
121-
expect(errorListener).toHaveBeenCalledWith('Refresh error');
122-
expect(l.error).toBe('Refresh error');
122+
expect(errorListener).toHaveBeenCalledWith(expect.any(Error));
123+
expect(l.error).toBeInstanceOf(Error);
124+
expect(l.errorMessage).toBe('Refresh error');
123125
expect(l.value).toBe('value-1');
124126

125127
errorListener.mockClear();
@@ -146,8 +148,9 @@ describe('LazyPromise', () => {
146148
errorListener.mockClear();
147149

148150
await l.promise;
149-
expect(l.error).toBe('Test error');
150-
expect(errorListener).toHaveBeenCalledWith('Test error');
151+
expect(l.error).toBeInstanceOf(Error);
152+
expect(l.errorMessage).toBe('Test error');
153+
expect(errorListener).toHaveBeenCalledWith(expect.any(Error));
151154

152155
errorListener.mockClear();
153156

@@ -385,7 +388,8 @@ describe('LazyPromise', () => {
385388

386389
try {
387390
await lazy.promise;
388-
expect(lazy.error).toBe('Fail');
391+
expect(lazy.error).toBeInstanceOf(Error);
392+
expect(lazy.errorMessage).toBe('Fail');
389393

390394
shouldFail = false;
391395
ref.setValue(2);

packages/common-mobx/src/structures/promiseCache.ts

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,8 +51,8 @@ export class PromiseCacheObservable<T, K = string> extends PromiseCache<T, K> {
5151
return new NumberModel();
5252
}
5353

54-
protected pure_createItemsCache(): IMapModel<string, T | null | undefined> {
55-
return observable.map<string, T | null | undefined>(undefined, { deep: false });
54+
protected pure_createItemsCache(): IMapModel<string, T | undefined> {
55+
return observable.map<string, T | undefined>(undefined, { deep: false });
5656
}
5757

5858
protected pure_createItemsStatus(): IMapModel<string, boolean> {
@@ -64,10 +64,7 @@ export class PromiseCacheObservable<T, K = string> extends PromiseCache<T, K> {
6464
}
6565

6666
/** @override */
67-
protected prepareResult(res: Awaited<T>) {
68-
if (res == null) {
69-
return res;
70-
}
67+
protected prepareResult(res: T) {
7168
return this._observeItems ? observable.object(res) : res;
7269
}
7370
}

packages/common-mobx/src/structures/subscribersPromiseCache.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { Disposable } from '@zajno/common/functions/disposer';
22
import type { Fields } from '@zajno/common/fields';
3+
import type { ILazyPromise } from '@zajno/common/lazy';
34
import { DeferredGetter } from '@zajno/common/structures/promiseCache';
45
import { SubscribersMapObservable } from './subscribersMap.js';
56
import { PromiseCacheObservable } from './promiseCache.js';
@@ -11,7 +12,11 @@ export type Fetcher<T> = (key: string, cb: (val: T) => Promise<void> | void) =>
1112
type ObserveStrategy = boolean | 'short' | number;
1213

1314
export interface IObservingCache<T> {
15+
/** @deprecated Use {@link getLazy} instead. */
1416
get(key: string): DeferredGetter<T>;
17+
18+
/** Returns an `ILazyPromise<T>` handle for the specified cache key. */
19+
getLazy(key: string): ILazyPromise<T>;
1520
}
1621

1722
export class SubscribersPromiseCache<T> extends Disposable implements IObservingCache<T> {
@@ -86,6 +91,27 @@ export class SubscribersPromiseCache<T> extends Disposable implements IObserving
8691
return this._cache.getDeferred(key);
8792
}
8893

94+
getLazy(key: string, overrideStrategy?: ObserveStrategy, observingStartedPromise?: (p: Promise<void>) => void): ILazyPromise<T> {
95+
if (overrideStrategy !== undefined) {
96+
this._observeStrategyOverrides[key] = overrideStrategy;
97+
}
98+
99+
const strategy = firstDefined(this._observeStrategyOverrides[key], this._observeStrategy);
100+
101+
if (strategy && !this._observers.getIsObserving(key)) {
102+
if (this._cache.hasKey(key)) {
103+
const timeout = getObserveTimeout(strategy);
104+
const promise = this._observers.enable(key, true, timeout);
105+
if (observingStartedPromise) {
106+
observingStartedPromise(promise);
107+
}
108+
promise.catch((err: Error) => this.logger?.error('[ObservingCache] Error on starting observe', key, strategy, err));
109+
}
110+
}
111+
112+
return this._cache.getLazy(key);
113+
}
114+
89115
populate = (key: string, item: T) => {
90116
this._updateItem(key, item);
91117
};

packages/common/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@zajno/common",
3-
"version": "2.8.8",
3+
"version": "2.8.9",
44
"description": "Zajno's re-usable utilities for JS/TS projects",
55
"private": false,
66
"type": "module",

packages/common/src/functions/safe.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,19 @@ export function wrapAsync<T, TArgs extends any[]>(fn: Nullable<(...args: TArgs)
1313
}
1414
};
1515
}
16+
17+
/**
18+
* Formats an unknown caught value into a human-readable string.
19+
*
20+
* Useful at presentation boundaries where a display string is needed.
21+
* Prefer keeping the raw error for programmatic handling (instanceof checks, .cause, etc.).
22+
*/
23+
export function formatError(err: unknown): string {
24+
if (typeof err === 'string') {
25+
return err;
26+
}
27+
if (err instanceof Error) {
28+
return err.message;
29+
}
30+
return String(err) || 'Unknown error';
31+
}

packages/common/src/lazy/__tests__/extensions.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,8 @@ describe('createCacheExtension', () => {
236236
await cached.promise; // Will reject but LazyPromise catches it
237237

238238
expect(storage.setValue).not.toHaveBeenCalled();
239-
expect(cached.error).toBe('Network error');
239+
expect(cached.error).toBeInstanceOf(Error);
240+
expect(cached.errorMessage).toBe('Network error');
240241
});
241242
});
242243

@@ -364,4 +365,3 @@ describe('createCacheExtension', () => {
364365
});
365366
});
366367
});
367-

packages/common/src/lazy/__tests__/lazy.test.ts

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -331,7 +331,8 @@ describe('LazyPromise', () => {
331331
const result = await lazy.refresh();
332332
expect(counter).toBe(2);
333333
expect(result.value).toBe(1);
334-
expect(lazy.error).toBe('Refresh failed');
334+
expect(lazy.error).toBeInstanceOf(Error);
335+
expect(lazy.errorMessage).toBe('Refresh failed');
335336
expect(lazy.value?.value).toBe(1);
336337

337338
shouldFail = false;
@@ -538,7 +539,9 @@ describe('LazyPromise', () => {
538539
expect(l.error).toBeNull();
539540
expect(l.value).toBeUndefined();
540541
expect(l.hasValue).toBeFalse();
541-
expect(l.error).toBe('Error object message');
542+
expect(l.error).toBeInstanceOf(Error);
543+
expect((l.error as Error).message).toBe('Error object message');
544+
expect(l.errorMessage).toBe('Error object message');
542545
}
543546

544547
{
@@ -547,9 +550,10 @@ describe('LazyPromise', () => {
547550
});
548551

549552
expect(l.value).toBeUndefined();
550-
expect(l.error).toBe('Factory error');
553+
expect(l.error).toBeInstanceOf(Error);
554+
expect(l.errorMessage).toBe('Factory error');
551555
expect(l.value).toBeUndefined();
552-
expect(l.error).toBe('Factory error');
556+
expect(l.errorMessage).toBe('Factory error');
553557
}
554558

555559
{
@@ -558,10 +562,12 @@ describe('LazyPromise', () => {
558562
});
559563

560564
expect(l.value).toBeUndefined();
561-
expect(l.error).toBe('error');
565+
expect(l.error).toBeInstanceOf(Error);
566+
expect(l.errorMessage).toBe('error');
562567

563568
l.reset();
564569
expect(l.error).toBeNull();
570+
expect(l.errorMessage).toBeNull();
565571
}
566572
});
567573

@@ -573,7 +579,8 @@ describe('LazyPromise', () => {
573579

574580
expect(l.error).toBeNull();
575581
await l.promise;
576-
expect(l.error).toBe('async error message');
582+
expect(l.error).toBeInstanceOf(Error);
583+
expect(l.errorMessage).toBe('async error message');
577584
expect(l.hasValue).toBeTrue();
578585
expect(l.value).toBeUndefined();
579586
}
@@ -584,7 +591,8 @@ describe('LazyPromise', () => {
584591
});
585592

586593
await l.promise;
587-
expect(l.error).toBe('async Error object');
594+
expect(l.error).toBeInstanceOf(Error);
595+
expect(l.errorMessage).toBe('async Error object');
588596
}
589597

590598
{
@@ -594,7 +602,8 @@ describe('LazyPromise', () => {
594602

595603
expect(l.value).toBe('initial value');
596604
await l.promise;
597-
expect(l.error).toBe('error occurred');
605+
expect(l.error).toBeInstanceOf(Error);
606+
expect(l.errorMessage).toBe('error occurred');
598607
expect(l.value).toBe('initial value');
599608
}
600609
});

0 commit comments

Comments
 (0)