-
-
Notifications
You must be signed in to change notification settings - Fork 22
/
LoadingCacheValue.mjs
140 lines (113 loc) · 4.29 KB
/
LoadingCacheValue.mjs
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
// @ts-check
/**
* @import { CacheEventMap, CacheKey, CacheValue } from "./Cache.mjs"
* @import { LoadingEventMap } from "./Loading.mjs"
*/
import Cache from "./Cache.mjs";
import cacheEntrySet from "./cacheEntrySet.mjs";
import Loading from "./Loading.mjs";
/**
* Controls loading a {@link CacheValue cache value}. It dispatches this
* sequence of events:
*
* 1. {@linkcode Loading} event {@link LoadingEventMap.start `start`}.
* 2. {@linkcode Cache} event {@link CacheEventMap.set `set`}.
* 3. {@linkcode Loading} event {@link LoadingEventMap.end `end`}.
*/
export default class LoadingCacheValue {
/**
* @param {Loading} loading Loading to update.
* @param {Cache} cache Cache to update.
* @param {CacheKey} cacheKey Cache key.
* @param {Promise<CacheValue>} loadingResult Resolves the loading result
* (including any loading errors) to be set as the
* {@link CacheValue cache value} if loading isn’t aborted. Shouldn’t
* reject.
* @param {AbortController} abortController Aborts this loading and skips
* setting the loading result as the {@link CacheValue cache value}. Has no
* effect after loading ends.
*/
constructor(loading, cache, cacheKey, loadingResult, abortController) {
if (!(loading instanceof Loading))
throw new TypeError("Argument 1 `loading` must be a `Loading` instance.");
if (!(cache instanceof Cache))
throw new TypeError("Argument 2 `cache` must be a `Cache` instance.");
if (typeof cacheKey !== "string")
throw new TypeError("Argument 3 `cacheKey` must be a string.");
if (!(loadingResult instanceof Promise))
throw new TypeError(
"Argument 4 `loadingResult` must be a `Promise` instance.",
);
if (!(abortController instanceof AbortController))
throw new TypeError(
"Argument 5 `abortController` must be an `AbortController` instance.",
);
/**
* When this loading started.
* @type {DOMHighResTimeStamp}
*/
this.timeStamp = performance.now();
/**
* Aborts this loading and skips setting the loading result as the
* {@link CacheValue cache value}. Has no effect after loading ends.
* @type {AbortController}
*/
this.abortController = abortController;
if (!(cacheKey in loading.store)) loading.store[cacheKey] = new Set();
const loadingSet = loading.store[cacheKey];
// In this constructor the instance must be synchronously added to the cache
// key’s loading set, so instances are set in the order they’re constructed
// and the loading store is updated for sync code following construction of
// a new instance.
/** @type {((value?: unknown) => void) | undefined} */
let loadingAddedResolve;
const loadingAdded = new Promise((resolve) => {
loadingAddedResolve = resolve;
});
/**
* Resolves the loading result, after the {@link CacheValue cache value} has
* been set if the loading wasn’t aborted. Shouldn’t reject.
* @type {Promise<CacheValue>}
*/
this.promise = loadingResult.then(async (result) => {
await loadingAdded;
if (
// The loading wasn’t aborted.
!this.abortController.signal.aborted
) {
// Before setting the cache value, await any earlier loading for the
// same cache key to to ensure events are emitted in order and that the
// last loading sets the final cache value.
let previousPromise;
for (const loadingCacheValue of loadingSet.values()) {
if (loadingCacheValue === this) {
// Harmless to await if it doesn’t exist.
await previousPromise;
break;
}
previousPromise = loadingCacheValue.promise;
}
cacheEntrySet(cache, cacheKey, result);
}
loadingSet.delete(this);
if (!loadingSet.size) delete loading.store[cacheKey];
loading.dispatchEvent(
new CustomEvent(`${cacheKey}/end`, {
detail: {
loadingCacheValue: this,
},
}),
);
return result;
});
loadingSet.add(this);
/** @type {(value?: unknown) => void} */ (loadingAddedResolve)();
loading.dispatchEvent(
new CustomEvent(`${cacheKey}/start`, {
detail: {
loadingCacheValue: this,
},
}),
);
}
}