This repository has been archived by the owner on Oct 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 75
/
cached.ts
112 lines (104 loc) · 3.56 KB
/
cached.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
import { DEBUG } from '@glimmer/env';
import { createCache, getValue } from '@glimmer/validator';
/**
* @decorator
*
* The `@cached` decorator can be used on getters in order to cache the return
* value of the getter. This is useful when a getter is expensive and used very
* often.
*
*
* @example
*
* in this guest list class, we have the `sortedGuests`
* getter that sorts the guests alphabetically:
*
* ```js
* import { tracked } from '@glimmer/tracking';
*
* class GuestList {
* @tracked guests = ['Zoey', 'Tomster'];
*
* get sortedGuests() {
* return this.guests.slice().sort()
* }
* }
* ```
*
* Every time `sortedGuests` is accessed, a new array will be created and sorted,
* because JavaScript getters do not cache by default. When the guest list is
* small, like the one in the example, this is not a problem. However, if the guest
* list were to grow very large, it would mean that we would be doing a large
* amount of work each time we accessed `sortedGetters`. With `@cached`, we can
* cache the value instead:
*
* ```js
* import { tracked, cached } from '@glimmer/tracking';
*
* class GuestList {
* @tracked guests = ['Zoey', 'Tomster'];
*
* @cached
* get sortedGuests() {
* return this.guests.slice().sort()
* }
* }
* ```
*
* Now the `sortedGuests` getter will be cached based on _autotracking_. It will
* only rerun and create a new sorted array when the `guests` tracked property is
* updated.
*
* In general, you should avoid using `@cached` unless you have confirmed that the
* getter you are decorating is computationally expensive. `@cached` adds a small
* amount of overhead to the getter, making it more expensive. While this overhead
* is small, if `@cached` is overused it can add up to a large impact overall in
* your app. Many getters and tracked properties are only accessed once, rendered,
* and then never rerendered, so adding `@cached` when it is unnecessary can
* negatively impact performance.
*/
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export const cached: PropertyDecorator = (...args: any[]) => {
const [target, key, descriptor] = args;
// Error on `@cached()`, `@cached(...args)`, and `@cached propName = value;`
if (DEBUG && target === undefined) throwCachedExtraneousParens();
if (
DEBUG &&
(typeof target !== 'object' ||
typeof key !== 'string' ||
typeof descriptor !== 'object' ||
args.length !== 3)
) {
throwCachedInvalidArgsError(args);
}
if (DEBUG && (!('get' in descriptor) || typeof descriptor.get !== 'function')) {
throwCachedGetterOnlyError(key);
}
const caches = new WeakMap();
const getter = descriptor.get;
descriptor.get = function (): unknown {
if (!caches.has(this)) {
caches.set(this, createCache(getter.bind(this)));
}
return getValue(caches.get(this));
};
};
function throwCachedExtraneousParens(): never {
throw new Error(
'You attempted to use @cached(), which is not necessary nor supported. Remove the parentheses and you will be good to go!'
);
}
function throwCachedGetterOnlyError(key: string): never {
throw new Error(`The @cached decorator must be applied to getters. '${key}' is not a getter.`);
}
function throwCachedInvalidArgsError(args: unknown[] = []): never {
throw new Error(
`You attempted to use @cached on with ${
args.length > 1 ? 'arguments' : 'an argument'
} ( @cached(${args
.map((d) => `'${d}'`)
.join(
', '
)}), which is not supported. Dependencies are automatically tracked, so you can just use ${'`@cached`'}`
);
}