-
Notifications
You must be signed in to change notification settings - Fork 250
Expand file tree
/
Copy pathconf-store.ts
More file actions
322 lines (276 loc) · 10 KB
/
conf-store.ts
File metadata and controls
322 lines (276 loc) · 10 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
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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
import {isLocalEnvironment} from './context/service.js'
import {isUnitTest} from '../../public/node/context/local.js'
import {LocalStorage} from '../../public/node/local-storage.js'
import {outputContent, outputDebug} from '../../public/node/output.js'
interface CacheValue<T> {
value: T
timestamp: number
}
export type PackageVersionKey = `npm-package-${string}`
export type NotificationsKey = `notifications-${string}`
export type NotificationKey = `notification-${string}`
export type GraphQLRequestKey = `q-${string}-${string}-${string}`
type MostRecentOccurrenceKey = `most-recent-occurrence-${string}`
type RateLimitKey = `rate-limited-occurrences-${string}`
type ExportedKey = PackageVersionKey | NotificationsKey | NotificationKey | GraphQLRequestKey
interface Cache {
[packageVersionKey: PackageVersionKey]: CacheValue<string>
[notifications: NotificationsKey]: CacheValue<string>
[notification: NotificationKey]: CacheValue<string>
[graphQLRequestKey: GraphQLRequestKey]: CacheValue<string>
[mostRecentOccurrenceKey: MostRecentOccurrenceKey]: CacheValue<boolean>
[rateLimitKey: RateLimitKey]: CacheValue<number[]>
}
export interface PendingDeviceAuth {
deviceCode: string
interval: number
expiresAt: number
verificationUriComplete: string
scopes: string[]
}
export interface ConfSchema {
sessionStore: string
currentSessionId?: string
devSessionStore?: string
currentDevSessionId?: string
cache?: Cache
pendingDeviceAuth?: PendingDeviceAuth
}
let _instance: LocalStorage<ConfSchema> | undefined
/**
* CLIKIT Store.
*
* @returns CLIKitStore.
*/
function cliKitStore() {
if (!_instance) {
_instance = new LocalStorage<ConfSchema>({projectName: `shopify-cli-kit${isUnitTest() ? '-test' : ''}`})
}
return _instance
}
function sessionStoreKey(): 'devSessionStore' | 'sessionStore' {
return isLocalEnvironment() ? 'devSessionStore' : 'sessionStore'
}
function currentSessionIdKey(): 'currentDevSessionId' | 'currentSessionId' {
return isLocalEnvironment() ? 'currentDevSessionId' : 'currentSessionId'
}
/**
* Get session.
*
* @returns Session.
*/
export function getSessions(config: LocalStorage<ConfSchema> = cliKitStore()): string | undefined {
outputDebug(outputContent`Getting session store...`)
return config.get(sessionStoreKey())
}
/**
* Set session.
*
* @param session - Session.
*/
export function setSessions(session: string, config: LocalStorage<ConfSchema> = cliKitStore()): void {
outputDebug(outputContent`Setting session store...`)
config.set(sessionStoreKey(), session)
}
/**
* Remove session.
*/
export function removeSessions(config: LocalStorage<ConfSchema> = cliKitStore()): void {
outputDebug(outputContent`Removing session store...`)
config.delete(sessionStoreKey())
}
/**
* Get current session ID.
*
* @returns Current session ID.
*/
export function getCurrentSessionId(config: LocalStorage<ConfSchema> = cliKitStore()): string | undefined {
outputDebug(outputContent`Getting current session ID...`)
return config.get(currentSessionIdKey())
}
/**
* Set current session ID.
*
* @param sessionId - Session ID.
*/
export function setCurrentSessionId(sessionId: string, config: LocalStorage<ConfSchema> = cliKitStore()): void {
outputDebug(outputContent`Setting current session ID...`)
config.set(currentSessionIdKey(), sessionId)
}
/**
* Remove current session ID.
*/
export function removeCurrentSessionId(config: LocalStorage<ConfSchema> = cliKitStore()): void {
outputDebug(outputContent`Removing current session ID...`)
config.delete(currentSessionIdKey())
}
/**
* Get pending device auth state (used for non-interactive login flow).
*/
export function getPendingDeviceAuth(config: LocalStorage<ConfSchema> = cliKitStore()): PendingDeviceAuth | undefined {
return config.get('pendingDeviceAuth')
}
/**
* Stash pending device auth state for later resumption.
*/
export function setPendingDeviceAuth(auth: PendingDeviceAuth, config: LocalStorage<ConfSchema> = cliKitStore()): void {
config.set('pendingDeviceAuth', auth)
}
/**
* Clear pending device auth state.
*/
export function clearPendingDeviceAuth(config: LocalStorage<ConfSchema> = cliKitStore()): void {
config.delete('pendingDeviceAuth')
}
type CacheValueForKey<TKey extends keyof Cache> = NonNullable<Cache[TKey]>['value']
/**
* Fetch from cache, or run the provided function to get the value, and cache it
* before returning it.
* @param key - The key to use for the cache.
* @param fn - The function to run to get the value to cache, if a cache miss occurs.
* @param timeout - The maximum valid age of a cached value, in milliseconds.
* If the cached value is older than this, it will be refreshed.
* @returns The value from the cache or the result of the function.
*/
export async function cacheRetrieveOrRepopulate(
key: ExportedKey,
fn: () => Promise<CacheValueForKey<typeof key>>,
timeout?: number,
config = cliKitStore(),
): Promise<CacheValueForKey<typeof key>> {
const cached = cacheRetrieve(key, config)
if (cached?.value !== undefined && (timeout === undefined || Date.now() - cached.timestamp < timeout)) {
return cached.value
}
const value = await fn()
cacheStore(key, value, config)
return value
}
export function cacheStore(key: ExportedKey, value: string, config = cliKitStore()): void {
const cache: Cache = config.get('cache') ?? {}
cache[key] = {value, timestamp: Date.now()}
config.set('cache', cache)
}
/**
* Fetch from cache if already populated, otherwise return undefined.
* @param key - The key to use for the cache.
* @returns The chache element.
*/
export function cacheRetrieve(key: ExportedKey, config = cliKitStore()): CacheValue<string> | undefined {
const cache: Cache = config.get('cache') ?? {}
return cache[key]
}
export function cacheClear(config = cliKitStore()): void {
config.delete('cache')
}
export interface TimeInterval {
days?: number
hours?: number
minutes?: number
seconds?: number
}
export function timeIntervalToMilliseconds({days = 0, hours = 0, minutes = 0, seconds = 0}: TimeInterval): number {
return (days * 24 * 60 * 60 + hours * 60 * 60 + minutes * 60 + seconds) * 1000
}
/**
* Execute a task only if the most recent occurrence of the task is older than the specified timeout.
* @param key - The key to use for the cache.
* @param timeout - The maximum valid age of the most recent occurrence, expressed as an object with
* days, hours, minutes, and seconds properties.
* If the most recent occurrence is older than this, the task will be executed.
* @param task - The task to run if the most recent occurrence is older than the timeout.
* @returns true if the task was run, or false if the task was not run.
*/
export async function runAtMinimumInterval(
key: string,
timeout: TimeInterval,
task: () => Promise<void>,
config = cliKitStore(),
): Promise<boolean> {
const cache: Cache = config.get('cache') ?? {}
const cacheKey: MostRecentOccurrenceKey = `most-recent-occurrence-${key}`
const cached = cache[cacheKey]
if (cached?.value !== undefined && Date.now() - cached.timestamp < timeIntervalToMilliseconds(timeout)) {
return false
}
await task()
cache[cacheKey] = {value: true, timestamp: Date.now()}
config.set('cache', cache)
return true
}
interface RunWithRateLimitOptions {
/**
* The key to use for the cache.
*/
key: string
/**
* The number of times the task can be run within the limit
*/
limit: number
/**
* The window of time after which the rate limit is refreshed,
* expressed as an object with days, hours, minutes, and seconds properties.
* If the most recent occurrence is older than this, the task will be executed.
*/
timeout: TimeInterval
/**
* The task to run if the most recent occurrence is older than the timeout.
*/
task: () => Promise<void>
}
/**
* Execute a task with a time-based rate limit. The rate limit is enforced by
* checking how many times that task has been executed in a window of time ending
* at the current time. If the task has been executed more than the allowed number
* of times in that window, the task will not be executed.
*
* Note that this function has side effects, as it will also remove events prior
* to the window of time that is being checked.
* @param options - The options for the rate limiting.
* @returns true, or undefined if the task was not run.
*/
export async function runWithRateLimit(options: RunWithRateLimitOptions, config = cliKitStore()): Promise<boolean> {
const {key, limit, timeout, task} = options
const cache: Cache = config.get('cache') ?? {}
const cacheKey: RateLimitKey = `rate-limited-occurrences-${key}`
const cached = cache[cacheKey]
const now = Date.now()
if (cached?.value) {
// First sweep through the cache and eliminate old events
const windowStart = now - timeIntervalToMilliseconds(timeout)
const occurrences = cached.value.filter((occurrence) => occurrence >= windowStart)
// Now check that the number of occurrences within the interval is below the limit
if (occurrences.length >= limit) {
// First remove the old occurrences from the cache
cache[cacheKey] = {value: occurrences, timestamp: Date.now()}
config.set('cache', cache)
return false
}
await task()
cache[cacheKey] = {value: [...occurrences, now], timestamp: now}
} else {
await task()
cache[cacheKey] = {value: [now], timestamp: now}
}
config.set('cache', cache)
return true
}
export function getConfigStoreForPartnerStatus() {
return new LocalStorage<Record<string, {status: true; checkedAt: string}>>({
projectName: 'shopify-cli-kit-partner-status',
})
}
export function getCachedPartnerAccountStatus(partnersToken: string): true | null {
if (!partnersToken) return null
const store = getConfigStoreForPartnerStatus()
const hasPartnerAccount = store.get(partnersToken)
if (hasPartnerAccount) {
// this never needs to expire
return true
}
return null
}
export function setCachedPartnerAccountStatus(partnersToken: string) {
const store = getConfigStoreForPartnerStatus()
store.set(partnersToken, {status: true, checkedAt: new Date().toISOString()})
}