-
Notifications
You must be signed in to change notification settings - Fork 42
/
promise.cacheable.decorator.ts
160 lines (150 loc) · 7.15 KB
/
promise.cacheable.decorator.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
import {empty, merge, Subject} from 'rxjs';
import {DEFAULT_CACHE_RESOLVER, ICacheable, GlobalCacheConfig, IStorageStrategy, DEFAULT_HASHER} from './common';
import {ICacheConfig} from './common/ICacheConfig';
import {ICachePair} from './common/ICachePair';
import {IAsyncStorageStrategy} from './common/IAsyncStorageStrategy';
export const promiseGlobalCacheBusterNotifier = new Subject<void>();
const getResponse = (oldMethod: Function, cacheKey: string, cacheConfig: ICacheConfig, context: any, cachePairs: ICachePair<any>[], parameters: any[], pendingCachePairs: ICachePair<Promise<any>>[] | {parameters: any; response: Promise<any>; created: Date;}[], storageStrategy: IStorageStrategy | IAsyncStorageStrategy, promiseImplementation: any) => {
let cacheParameters = cacheConfig.cacheHasher(parameters);
let _foundCachePair = cachePairs.find(cp =>
cacheConfig.cacheResolver(cp.parameters, cacheParameters)
);
const _foundPendingCachePair = pendingCachePairs.find(cp =>
cacheConfig.cacheResolver(cp.parameters, cacheParameters)
);
/**
* check if maxAge is passed and cache has actually expired
*/
if ((cacheConfig.maxAge || GlobalCacheConfig.maxAge) && _foundCachePair && _foundCachePair.created) {
if (
new Date().getTime() - new Date(_foundCachePair.created).getTime() >
(cacheConfig.maxAge || GlobalCacheConfig.maxAge)
) {
/**
* cache duration has expired - remove it from the cachePairs array
*/
storageStrategy.remove ? storageStrategy.remove(cachePairs.indexOf(_foundCachePair), _foundCachePair, cacheKey, this) : storageStrategy.removeAtIndex(cachePairs.indexOf(_foundCachePair), cacheKey, this);
_foundCachePair = null;
} else if (cacheConfig.slidingExpiration || GlobalCacheConfig.slidingExpiration) {
/**
* renew cache duration
*/
_foundCachePair.created = new Date();
storageStrategy.update ? storageStrategy.update(cachePairs.indexOf(_foundCachePair), _foundCachePair, cacheKey, this) : storageStrategy.updateAtIndex(cachePairs.indexOf(_foundCachePair), _foundCachePair, cacheKey, this);
}
}
if (_foundCachePair) {
return promiseImplementation.resolve(_foundCachePair.response);
} else if (_foundPendingCachePair) {
return _foundPendingCachePair.response;
} else {
const response$ = (oldMethod.call(context, ...parameters) as Promise<any>)
.then(response => {
removeCachePair(pendingCachePairs, parameters, cacheConfig);
/**
* if no maxCacheCount has been passed
* if maxCacheCount has not been passed, just shift the cachePair to make room for the new one
* if maxCacheCount has been passed, respect that and only shift the cachePairs if the new cachePair will make them exceed the count
*/
if (
!cacheConfig.shouldCacheDecider ||
cacheConfig.shouldCacheDecider(response)
) {
if (
!(cacheConfig.maxCacheCount || GlobalCacheConfig.maxCacheCount) ||
(cacheConfig.maxCacheCount || GlobalCacheConfig.maxCacheCount) === 1 ||
((cacheConfig.maxCacheCount || GlobalCacheConfig.maxCacheCount) &&
(cacheConfig.maxCacheCount || GlobalCacheConfig.maxCacheCount) < cachePairs.length + 1)
) {
storageStrategy.remove ? storageStrategy.remove(0, cachePairs[0], cacheKey, this) : storageStrategy.removeAtIndex(0, cacheKey, this);
}
storageStrategy.add({
parameters: cacheParameters,
response,
created: (cacheConfig.maxAge || GlobalCacheConfig.maxAge) ? new Date() : null
}, cacheKey, this);
}
return response;
})
.catch(error => {
removeCachePair(pendingCachePairs, parameters, cacheConfig);
return promiseImplementation.reject(error);
});
/**
* cache the stream
*/
pendingCachePairs.push({
parameters: cacheParameters,
response: response$,
created: new Date()
});
return response$;
}
}
const removeCachePair = <T>(
cachePairs: Array<ICachePair<T>>,
parameters: any,
cacheConfig: ICacheConfig
) => {
const cacheParameters = cacheConfig.cacheHasher(parameters);
/**
* if there has been an pending cache pair for these parameters, when it completes or errors, remove it
*/
const _pendingCachePairToRemove = cachePairs.find(cp =>
cacheConfig.cacheResolver(cp.parameters, cacheParameters)
);
cachePairs.splice(cachePairs.indexOf(_pendingCachePairToRemove), 1);
};
export function PCacheable(cacheConfig: ICacheConfig = {}) {
return function(
_target: Object,
_propertyKey: string,
propertyDescriptor: TypedPropertyDescriptor<ICacheable<Promise<any>>>
) {
const cacheKey = cacheConfig.cacheKey || _target.constructor.name + '#' + _propertyKey;
const oldMethod = propertyDescriptor.value;
if (propertyDescriptor && propertyDescriptor.value) {
let storageStrategy: IStorageStrategy | IAsyncStorageStrategy = !cacheConfig.storageStrategy
? new GlobalCacheConfig.storageStrategy()
: new cacheConfig.storageStrategy();
const pendingCachePairs: Array<ICachePair<Promise<any>>> = [];
if (cacheConfig.cacheModifier) {
cacheConfig.cacheModifier.subscribe(async callback => storageStrategy.addMany(callback(await storageStrategy.getAll(cacheKey, this)), cacheKey, this))
}
/**
* subscribe to the promiseGlobalCacheBusterNotifier
* if a custom cacheBusterObserver is passed, subscribe to it as well
* subscribe to the cacheBusterObserver and upon emission, clear all caches
*/
merge(
promiseGlobalCacheBusterNotifier.asObservable(),
cacheConfig.cacheBusterObserver
? cacheConfig.cacheBusterObserver
: empty()
).subscribe(_ => {
storageStrategy.removeAll(cacheKey, this);
pendingCachePairs.length = 0;
});
const cacheResolver = cacheConfig.cacheResolver || GlobalCacheConfig.cacheResolver;
cacheConfig.cacheResolver = cacheResolver
? cacheResolver
: DEFAULT_CACHE_RESOLVER;
const cacheHasher = cacheConfig.cacheHasher || GlobalCacheConfig.cacheHasher;
cacheConfig.cacheHasher = cacheHasher
? cacheHasher
: DEFAULT_HASHER;
/* use function instead of an arrow function to keep context of invocation */
(propertyDescriptor.value as any) = function(...parameters: Array<any>) {
const promiseImplementation = typeof GlobalCacheConfig.promiseImplementation === 'function' && (GlobalCacheConfig.promiseImplementation !== Promise) ?
(GlobalCacheConfig.promiseImplementation as () => PromiseConstructorLike).call(this)
: GlobalCacheConfig.promiseImplementation as PromiseConstructorLike;
let cachePairs = storageStrategy.getAll(cacheKey, this);
if (!(cachePairs instanceof promiseImplementation)) {
cachePairs = promiseImplementation.resolve(cachePairs);
}
return (cachePairs as Promise<ICachePair<any>[]>).then(cachePairs => getResponse(oldMethod, cacheKey, cacheConfig, this, cachePairs, parameters, pendingCachePairs, storageStrategy, promiseImplementation))
};
}
return propertyDescriptor;
};
};