@sjcrh/proteinpaint-shared 2.175.0 → 2.176.1-0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/fetch-helpers.js +71 -54
- package/src/helpers.js +9 -0
package/package.json
CHANGED
package/src/fetch-helpers.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { hash } from './hash.js'
|
|
2
2
|
import { encode } from './urljson.js'
|
|
3
|
+
import { deepFreeze } from './helpers.js'
|
|
3
4
|
|
|
4
5
|
/*
|
|
5
6
|
ezFetch()
|
|
@@ -165,17 +166,15 @@ async function processNDJSON_nestedKey(r) {
|
|
|
165
166
|
return rootObj
|
|
166
167
|
}
|
|
167
168
|
|
|
168
|
-
// key: request object reference or
|
|
169
|
-
// value:
|
|
169
|
+
// key: request object reference or computed string dataName
|
|
170
|
+
// value: {
|
|
171
|
+
// response: fetch promise or response,
|
|
172
|
+
// exp: expiration timestamp
|
|
173
|
+
// }
|
|
170
174
|
const dataCache = new Map()
|
|
171
|
-
//
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
// when caching by string dataName, track entries to manage the cache size
|
|
175
|
-
const cachedDataNames = []
|
|
176
|
-
// maximum number of cached dataNames, oldest will be deleted if 1000 is exceeded
|
|
177
|
-
const maxNumOfDataKeys = 360
|
|
178
|
-
|
|
175
|
+
// maximum number of cached dataNames, oldest will be deleted if this is exceeded
|
|
176
|
+
const maxNumOfDataKeys = 10
|
|
177
|
+
const cacheLifetime = 1000 * 60 * 5
|
|
179
178
|
/*
|
|
180
179
|
memFetch()
|
|
181
180
|
- fetch wrapper that saves cached responses into memory and recovers them for matching subsequent requests
|
|
@@ -188,65 +187,83 @@ const maxNumOfDataKeys = 360
|
|
|
188
187
|
url
|
|
189
188
|
init{headers?, body?}
|
|
190
189
|
- first two arguments are same as native fetch
|
|
190
|
+
- when passing opts.client, may include other applicable options inside the init{} object, such as retry
|
|
191
191
|
|
|
192
|
-
opts{
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
192
|
+
opts{client}
|
|
193
|
+
|
|
194
|
+
client: use this http client instead of native fetch
|
|
195
|
+
- since fetch-helpers is shared between server and frontend workspaces,
|
|
196
|
+
cannot directly import non-native modules at the beginning of this code file
|
|
197
|
+
- for server side usage, client may be `xfetch()`, `ky` or other libraries
|
|
196
198
|
*/
|
|
197
199
|
export async function memFetch(url, init, opts = {}) {
|
|
198
200
|
if (typeof init.body === 'object') init.body = JSON.stringify(init.body)
|
|
199
201
|
const dataKey = opts.q || (await getDataName(url, init))
|
|
200
|
-
|
|
202
|
+
const { response, exp } = dataCache.get(dataKey) || {}
|
|
203
|
+
const now = Date.now()
|
|
204
|
+
let result = response // either a Promise or actual data
|
|
201
205
|
|
|
202
|
-
if (
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
206
|
+
if (result) {
|
|
207
|
+
// extend the expiration, since exp is more about managing the cache size
|
|
208
|
+
// and not the validity of the cached response. A response for the current
|
|
209
|
+
// dataName req.url + body + headers is technically valid until a new data version
|
|
210
|
+
// gets published.
|
|
211
|
+
dataCache.set(dataKey, { response, exp: now + cacheLifetime })
|
|
212
|
+
return result
|
|
213
|
+
} else {
|
|
208
214
|
try {
|
|
209
|
-
// do not await so that this same promise may be reused
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
215
|
+
// IMPORTANT: do not await so that this same promise may be reused
|
|
216
|
+
// by subsequent requests with the same dataKey
|
|
217
|
+
result = opts.client
|
|
218
|
+
? opts.client(url, init, Object.assign(opts, { client: undefined })).then(response => {
|
|
219
|
+
// replace the cached promise result with the actual data,
|
|
220
|
+
// since persisting a cached promise for a long time is likely not best practice
|
|
221
|
+
dataCache.set(dataKey, { response, exp: Date.now() + cacheLifetime })
|
|
222
|
+
return response
|
|
223
|
+
})
|
|
224
|
+
: fetch(url, init).then(async r => {
|
|
225
|
+
const response = await processResponse(r)
|
|
226
|
+
if (!r.ok) {
|
|
227
|
+
console.trace(response)
|
|
228
|
+
throw (
|
|
229
|
+
'memFetch error ' +
|
|
230
|
+
r.status +
|
|
231
|
+
': ' +
|
|
232
|
+
(typeof response == 'object' ? response.message || response.error : response)
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
// replace the cached promise result with the actual data,
|
|
236
|
+
// since persisting a cached promise for a long time is likely not best practice
|
|
237
|
+
dataCache.set(dataKey, { response: deepFreeze(response), exp: Date.now() + cacheLifetime })
|
|
238
|
+
return response
|
|
239
|
+
})
|
|
240
|
+
|
|
241
|
+
dataCache.set(dataKey, { response: result, exp: Date.now() + cacheLifetime })
|
|
242
|
+
manageCacheSize(now)
|
|
243
|
+
return result
|
|
229
244
|
} catch (e) {
|
|
230
|
-
delete
|
|
245
|
+
// delete this cache only if it is a promise;
|
|
246
|
+
// do not delete a valid resolved data cache
|
|
247
|
+
if (dataCache.get(dataKey) instanceof Promise) delete dataCache.delete(dataKey)
|
|
231
248
|
throw e
|
|
232
249
|
}
|
|
233
250
|
}
|
|
234
|
-
if (typeof dataKey === 'string') manageCacheSize(dataKey)
|
|
235
|
-
return result
|
|
236
251
|
}
|
|
237
252
|
|
|
238
253
|
export function deleteCache(key) {
|
|
239
|
-
|
|
254
|
+
dataCache.delete(key)
|
|
240
255
|
}
|
|
241
256
|
|
|
242
|
-
export function manageCacheSize(
|
|
243
|
-
|
|
244
|
-
const
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
257
|
+
export function manageCacheSize(_now) {
|
|
258
|
+
const now = _now || Date.now()
|
|
259
|
+
const keyExp = []
|
|
260
|
+
for (const [key, result] of dataCache.entries()) {
|
|
261
|
+
if (result.exp < now) dataCache.delete(key)
|
|
262
|
+
else keyExp.push({ key, exp: result.exp })
|
|
263
|
+
}
|
|
264
|
+
if (dataCache.size > maxNumOfDataKeys) {
|
|
265
|
+
const oldestEntries = keyExp.sort((a, b) => a.exp - b.exp).slice(maxNumOfDataKeys)
|
|
266
|
+
for (const entry of oldestEntries) dataCache.delete(entry.key)
|
|
250
267
|
}
|
|
251
268
|
}
|
|
252
269
|
|
|
@@ -261,7 +278,7 @@ export async function getDataName(url, init) {
|
|
|
261
278
|
}
|
|
262
279
|
|
|
263
280
|
//
|
|
264
|
-
export function
|
|
281
|
+
export function clearMemFetchDataCache(opts = {}) {
|
|
265
282
|
if (!opts.serverData) {
|
|
266
283
|
dataCache.clear()
|
|
267
284
|
return
|
package/src/helpers.js
CHANGED
|
@@ -59,6 +59,15 @@ export function deepEqual(x, y) {
|
|
|
59
59
|
} else return false
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
export function deepFreeze(obj) {
|
|
63
|
+
Object.freeze(obj)
|
|
64
|
+
// not using for..in loop, in order to not descend into inherited props/methods
|
|
65
|
+
for (const value of Object.values(obj)) {
|
|
66
|
+
if (value !== null && typeof value == 'object') deepFreeze(value)
|
|
67
|
+
}
|
|
68
|
+
return obj
|
|
69
|
+
}
|
|
70
|
+
|
|
62
71
|
export class CustomError extends Error {
|
|
63
72
|
level = '' // '' | 'warn'
|
|
64
73
|
|