-
-
Notifications
You must be signed in to change notification settings - Fork 61
Expand file tree
/
Copy pathindex.ts
More file actions
137 lines (114 loc) 路 4.1 KB
/
Copy pathindex.ts
File metadata and controls
137 lines (114 loc) 路 4.1 KB
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
import { type Requester } from '@logto/js';
import { trySafe, type Nullable, conditional } from '@silverhand/essentials';
import {
type CacheKey,
type Navigate,
type PersistKey,
type Storage,
type StorageKey,
type ClientAdapter,
type InferStorageKey,
} from './types.js';
// Track in-flight cache writes per cache storage instance. A WeakMap keeps this helper from
// extending the lifetime of adapter-provided cache stores.
const runningCacheGetters = new WeakMap<Storage<CacheKey>, Map<CacheKey, Promise<unknown>>>();
const getRunningCacheGetterMap = (cache: Storage<CacheKey>) => {
const runningGetterMap = runningCacheGetters.get(cache);
if (runningGetterMap) {
return runningGetterMap;
}
const newRunningGetterMap = new Map<CacheKey, Promise<unknown>>();
runningCacheGetters.set(cache, newRunningGetterMap);
return newRunningGetterMap;
};
export class ClientAdapterInstance implements ClientAdapter {
/*
* Implement `ClientAdapter`. Its properties are assigned by
* `Object.assign()` in the constructor.
*/
requester!: Requester;
storage!: Storage<StorageKey | PersistKey>;
unstable_cache?: Storage<CacheKey> | undefined;
navigate!: Navigate;
generateState!: () => string | Promise<string>;
generateCodeVerifier!: () => string | Promise<string>;
generateCodeChallenge!: (codeVerifier: string) => string | Promise<string>;
/* END OF IMPLEMENTATION */
constructor(adapter: ClientAdapter) {
// eslint-disable-next-line @silverhand/fp/no-mutating-assign
Object.assign(this, adapter);
}
async setStorageItem(key: InferStorageKey<typeof this.storage>, value: Nullable<string>) {
if (!value) {
await this.storage.removeItem(key);
return;
}
await this.storage.setItem(key, value);
}
/**
* Try to get the string value from the cache and parse as JSON.
* Return the parsed value if it is an object, return `undefined` otherwise.
*
* @param key The cache key to get value from.
*/
async getCachedObject<T>(key: CacheKey): Promise<T | undefined> {
const cached = await trySafe(async () => {
const data = await this.unstable_cache?.getItem(key);
// It's actually `unknown`
// eslint-disable-next-line no-restricted-syntax
return conditional(data && (JSON.parse(data) as unknown));
});
if (cached && typeof cached === 'object') {
// Trust cache for now
// eslint-disable-next-line no-restricted-syntax
return cached as T;
}
}
/**
* Try to get the value from the cache first, if it doesn't exist in cache,
* run the getter function and store the result into cache.
*
* @param key The cache key to get value from.
*/
async getWithCache<T>(key: CacheKey, getter: () => Promise<T>): Promise<T> {
const cached = await this.getCachedObject<T>(key);
if (cached) {
return cached;
}
const { unstable_cache: cache } = this;
if (!cache) {
return getter();
}
const runningGetterMap = getRunningCacheGetterMap(cache);
const runningGetter = runningGetterMap.get(key);
if (runningGetter) {
// Another client sharing the same cache storage is already populating this key. Wait for it
// instead of issuing a duplicate discovery request.
try {
await runningGetter;
} catch {
// The in-flight getter rejected before writing to cache. Fall through to the cache check
// and the current caller's getter below.
}
const cachedResult = await this.getCachedObject<T>(key);
if (cachedResult) {
return cachedResult;
}
// The in-flight getter may fail before writing to cache. Retry through the same population
// path so a successful recovery is stored for later callers.
return this.getWithCache(key, getter);
}
const newRunningGetter = (async () => {
const result = await getter();
await cache.setItem(key, JSON.stringify(result));
return result;
})();
runningGetterMap.set(key, newRunningGetter);
try {
return await newRunningGetter;
} finally {
runningGetterMap.delete(key);
}
}
}
export * from './types.js';