-
Notifications
You must be signed in to change notification settings - Fork 19
/
Copy pathindex.ts
246 lines (214 loc) · 9.21 KB
/
index.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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
import { Trie } from "@wry/trie";
import { StrongCache, CommonCache } from "@wry/caches";
import { Entry, AnyEntry } from "./entry.js";
import { parentEntrySlot } from "./context.js";
import type { NoInfer } from "./helpers.js";
// These helper functions are important for making optimism work with
// asynchronous code. In order to register parent-child dependencies,
// optimism needs to know about any currently active parent computations.
// In ordinary synchronous code, the parent context is implicit in the
// execution stack, but asynchronous code requires some extra guidance in
// order to propagate context from one async task segment to the next.
export {
bindContext,
noContext,
nonReactive,
setTimeout,
asyncFromGen,
Slot,
} from "./context.js";
// A lighter-weight dependency, similar to OptimisticWrapperFunction, except
// with only one argument, no makeCacheKey, no wrapped function to recompute,
// and no result value. Useful for representing dependency leaves in the graph
// of computation. Subscriptions are supported.
export { dep, OptimisticDependencyFunction } from "./dep.js";
// The defaultMakeCacheKey function is remarkably powerful, because it gives
// a unique object for any shallow-identical list of arguments. If you need
// to implement a custom makeCacheKey function, you may find it helpful to
// delegate the final work to defaultMakeCacheKey, which is why we export it
// here. However, you may want to avoid defaultMakeCacheKey if your runtime
// does not support WeakMap, or you have the ability to return a string key.
// In those cases, just write your own custom makeCacheKey functions.
let defaultKeyTrie: Trie<object> | undefined;
export function defaultMakeCacheKey(...args: any[]): object {
const trie = defaultKeyTrie || (
defaultKeyTrie = new Trie(typeof WeakMap === "function")
);
return trie.lookupArray(args);
}
// If you're paranoid about memory leaks, or you want to avoid using WeakMap
// under the hood, but you still need the behavior of defaultMakeCacheKey,
// import this constructor to create your own tries.
export { Trie as KeyTrie }
export type OptimisticWrapperFunction<
TArgs extends any[],
TResult,
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
> = ((...args: TArgs) => TResult) & {
// Get the current number of Entry objects in the LRU cache.
readonly size: number;
// Snapshot of wrap options used to create this wrapper function.
options: OptionsWithCacheInstance<TArgs, TKeyArgs, TCacheKey>;
// "Dirty" any cached Entry stored for the given arguments, marking that Entry
// and its ancestors as potentially needing to be recomputed. The .dirty(...)
// method of an optimistic function takes the same parameter types as the
// original function by default, unless a keyArgs function is configured, and
// then it matters that .dirty takes TKeyArgs instead of TArgs.
dirty: (...args: TKeyArgs) => void;
// A version of .dirty that accepts a key returned by .getKey.
dirtyKey: (key: TCacheKey | undefined) => void;
// Examine the current value without recomputing it.
peek: (...args: TKeyArgs) => TResult | undefined;
// A version of .peek that accepts a key returned by .getKey.
peekKey: (key: TCacheKey | undefined) => TResult | undefined;
// Completely remove the entry from the cache, dirtying any parent entries.
forget: (...args: TKeyArgs) => boolean;
// A version of .forget that accepts a key returned by .getKey.
forgetKey: (key: TCacheKey | undefined) => boolean;
// In order to use the -Key version of the above functions, you need a key
// rather than the arguments used to compute the key. These two functions take
// TArgs or TKeyArgs and return the corresponding TCacheKey. If no keyArgs
// function has been configured, TArgs will be the same as TKeyArgs, and thus
// getKey and makeCacheKey will be synonymous.
getKey: (...args: TArgs) => TCacheKey | undefined;
// This property is equivalent to the makeCacheKey function provided in the
// OptimisticWrapOptions, or (if no options.makeCacheKey function is provided)
// a default implementation of makeCacheKey. This function is also exposed as
// optimistic.options.makeCacheKey, somewhat redundantly.
makeCacheKey: (...args: TKeyArgs) => TCacheKey | undefined;
};
export { CommonCache }
export interface CommonCacheConstructor<TCacheKey, TResult, TArgs extends any[]> extends Function {
new <K extends TCacheKey, V extends Entry<TArgs, TResult>>(max?: number, dispose?: (value: V, key?: K) => void): CommonCache<K,V>;
}
export type OptimisticWrapOptions<
TArgs extends any[],
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
TResult = any,
> = {
// The maximum number of cache entries that should be retained before the
// cache begins evicting the oldest ones.
max?: number;
// Transform the raw arguments to some other type of array, which will then
// be passed to makeCacheKey.
keyArgs?: (...args: TArgs) => TKeyArgs;
// The makeCacheKey function takes the same arguments that were passed to
// the wrapper function and returns a single value that can be used as a key
// in a Map to identify the cached result.
makeCacheKey?: (...args: NoInfer<TKeyArgs>) => TCacheKey | undefined;
// Called when a new value is computed to allow efficient normalization of
// results over time, for example by returning older if equal(newer, older).
normalizeResult?: (newer: TResult, older: TResult) => TResult;
// If provided, the subscribe function should either return an unsubscribe
// function or return nothing.
subscribe?: (...args: TArgs) => void | (() => any);
cache?: CommonCache<NoInfer<TCacheKey>, Entry<NoInfer<TArgs>, NoInfer<TResult>>>
| CommonCacheConstructor<NoInfer<TCacheKey>, NoInfer<TResult>, NoInfer<TArgs>>;
};
export interface OptionsWithCacheInstance<
TArgs extends any[],
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
TResult = any,
> extends OptimisticWrapOptions<TArgs, TKeyArgs, TCacheKey, TResult> {
cache: CommonCache<NoInfer<TCacheKey>, Entry<NoInfer<TArgs>, NoInfer<TResult>>>;
};
const caches = new Set<CommonCache<any, AnyEntry>>();
export function wrap<
TArgs extends any[],
TResult,
TKeyArgs extends any[] = TArgs,
TCacheKey = any,
>(originalFunction: (...args: TArgs) => TResult, {
max = Math.pow(2, 16),
keyArgs,
makeCacheKey = (defaultMakeCacheKey as () => TCacheKey),
normalizeResult,
subscribe,
cache: cacheOption = StrongCache,
}: OptimisticWrapOptions<TArgs, TKeyArgs, TCacheKey, TResult> = Object.create(null)) {
const cache: CommonCache<TCacheKey, Entry<TArgs, TResult>> =
typeof cacheOption === "function"
? new cacheOption(max, entry => entry.dispose())
: cacheOption;
const optimistic = function (): TResult {
const key = makeCacheKey.apply(
null,
keyArgs ? keyArgs.apply(null, arguments as any) : arguments as any
);
if (key === void 0) {
return originalFunction.apply(null, arguments as any);
}
let entry = cache.get(key)!;
if (!entry) {
cache.set(key, entry = new Entry(originalFunction));
entry.normalizeResult = normalizeResult;
entry.subscribe = subscribe;
// Give the Entry the ability to trigger cache.delete(key), even though
// the Entry itself does not know about key or cache.
entry.forget = () => cache.delete(key);
}
const value = entry.recompute(
Array.prototype.slice.call(arguments) as TArgs,
);
// Move this entry to the front of the least-recently used queue,
// since we just finished computing its value.
cache.set(key, entry);
caches.add(cache);
// Clean up any excess entries in the cache, but only if there is no
// active parent entry, meaning we're not in the middle of a larger
// computation that might be flummoxed by the cleaning.
if (! parentEntrySlot.hasValue()) {
caches.forEach(cache => cache.clean());
caches.clear();
}
return value;
} as OptimisticWrapperFunction<TArgs, TResult, TKeyArgs, TCacheKey>;
Object.defineProperty(optimistic, "size", {
get: () => cache.size,
configurable: false,
enumerable: false,
});
Object.freeze(optimistic.options = {
max,
keyArgs,
makeCacheKey,
normalizeResult,
subscribe,
cache,
});
function dirtyKey(key: TCacheKey | undefined) {
const entry = key && cache.get(key);
if (entry) {
entry.setDirty();
}
}
optimistic.dirtyKey = dirtyKey;
optimistic.dirty = function dirty() {
dirtyKey(makeCacheKey.apply(null, arguments as any));
};
function peekKey(key: TCacheKey | undefined) {
const entry = key && cache.get(key);
if (entry) {
return entry.peek();
}
}
optimistic.peekKey = peekKey;
optimistic.peek = function peek() {
return peekKey(makeCacheKey.apply(null, arguments as any));
};
function forgetKey(key: TCacheKey | undefined) {
return key ? cache.delete(key) : false;
}
optimistic.forgetKey = forgetKey;
optimistic.forget = function forget() {
return forgetKey(makeCacheKey.apply(null, arguments as any));
};
optimistic.makeCacheKey = makeCacheKey;
optimistic.getKey = keyArgs ? function getKey() {
return makeCacheKey.apply(null, keyArgs.apply(null, arguments as any));
} : makeCacheKey as (...args: any[]) => TCacheKey | undefined;
return Object.freeze(optimistic);
}