@sjcrh/proteinpaint-shared 2.175.0 → 2.177.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sjcrh/proteinpaint-shared",
3
- "version": "2.175.0",
3
+ "version": "2.177.0",
4
4
  "description": "ProteinPaint code that is shared between server and client-side workspaces",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
package/src/common.js CHANGED
@@ -17,21 +17,22 @@ import { getWrappedTvslst } from './filter.js'
17
17
  // part of 'common' argument to exported dataset js function, at server runtime
18
18
  export const TermTypeGroups = {
19
19
  DICTIONARY_VARIABLES: 'Dictionary Variables',
20
- MUTATION_CNV_FUSION: 'Mutation/CNV/Fusion',
21
- VARIANT_GENOTYPE: 'Variant Genotype',
22
20
  DNA_METHYLATION: 'DNA Methylation',
23
21
  GENE_DEPENDENCY: 'Gene Dependency',
24
22
  GENE_EXPRESSION: 'Gene Expression',
25
- PROTEIN_EXPRESSION: 'Protein Expression',
26
- SPLICE_JUNCTION: 'Splice Junction',
27
- METABOLITE_INTENSITY: 'Metabolite Intensity',
28
23
  GSEA: 'GSEA',
24
+ METABOLITE_INTENSITY: 'Metabolite Intensity',
25
+ MUTATION_CNV_FUSION: 'Mutation/CNV/Fusion',
29
26
  MUTATION_SIGNATURE: 'Mutation Signature',
27
+ PROTEIN_EXPRESSION: 'Protein Expression',
28
+ SINGLECELL_CELLTYPE: 'Single-cell Cell Type',
30
29
  SNP: 'SNP Genotype',
31
30
  SNP_LIST: 'SNP List',
32
31
  SNP_LOCUS: 'SNP Locus',
32
+ SPLICE_JUNCTION: 'Splice Junction',
33
33
  SSGSEA: 'Geneset Expression',
34
- TERM_COLLECTION: 'Term Collection'
34
+ TERM_COLLECTION: 'Term Collection',
35
+ VARIANT_GENOTYPE: 'Variant Genotype'
35
36
  }
36
37
 
37
38
  export const defaultcolor = rgb('#8AB1D4').darker()
@@ -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 conputed string dataName
169
- // value: fetch promise or response
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
- // NOTE: when caching by request object reference,
172
- // consumer code must call deleteCache(q) at the end of the request handling
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{q}
193
- q?: request object (passed by reference, not copy)
194
- - if provided, will be used as cache data key
195
- - if not provided, then a string cache data key will be computed from the request url, body, headers
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
- let result = dataCache.get(dataKey)
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 (!result || (typeof result != 'object' && !(result instanceof Promise))) {
203
- dataCache.delete(dataKey)
204
- result = undefined
205
- }
206
-
207
- if (!result) {
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 by all subsequent requests with the same dataKey
210
- dataCache.set(
211
- dataKey,
212
- fetch(url, init).then(async r => {
213
- const response = await processResponse(r)
214
- if (!r.ok) {
215
- console.trace(response)
216
- throw (
217
- 'memFetch error ' +
218
- r.status +
219
- ': ' +
220
- (typeof response == 'object' ? response.message || response.error : response)
221
- )
222
- }
223
- // to-do: support opt.freeze to enforce deep freeze of data.json()
224
- dataCache.set(dataKey, response)
225
- return dataCache.get(dataKey)
226
- })
227
- )
228
- result = dataCache.get(dataKey)
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 dataCache.delete(dataKey)
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
- delete dataCache.delete(key)
254
+ dataCache.delete(key)
240
255
  }
241
256
 
242
- export function manageCacheSize(dataKey) {
243
- // manage the number of stored keys in dataCache
244
- const i = cachedDataNames.indexOf(dataKey)
245
- if (i !== -1) cachedDataNames.splice(i, 1) // if the dataKey already exists, delete from current place in tracking array to move to front
246
- cachedDataNames.unshift(dataKey) // add the dataKey to the front of the tracking array
247
- while (cachedDataNames.length > maxNumOfDataKeys) {
248
- const oldestDataname = cachedDataNames.pop() // delete the dataKey from the tracking array
249
- dataCache.delete(oldestDataname)
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 clearCache(opts = {}) {
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
 
@@ -113,23 +113,7 @@ export function isUsableTerm(term, _usecase, termdbConfig, ds) {
113
113
  }
114
114
  return uses
115
115
  case 'runChart2':
116
- if (usecase.detail == 'date') {
117
- if (term.type == 'date') {
118
- uses.add('plot')
119
- }
120
- if (child_types.includes('date')) uses.add('branch')
121
- } else if (usecase.detail == 'numeric') {
122
- if (isNumericTerm(term) && term.type != 'date') {
123
- uses.add('plot')
124
- }
125
- if (hasNumericChild(child_types)) uses.add('branch')
126
- } else {
127
- if (graphableTypes.has(term.type)) uses.add('plot')
128
- if (!term.isleaf) uses.add('branch')
129
- }
130
- return uses
131
- case 'frequencyChart':
132
- if (usecase.detail == 'term') {
116
+ if (usecase.detail == 'date' || usecase.detail == 'xtw') {
133
117
  if (term.type == 'date') {
134
118
  uses.add('plot')
135
119
  }
package/src/terms.js CHANGED
@@ -70,7 +70,8 @@ export const typeGroup = {
70
70
  [TermTypes.GENE_EXPRESSION]: TermTypeGroups.GENE_EXPRESSION,
71
71
  [TermTypes.SSGSEA]: TermTypeGroups.SSGSEA,
72
72
  [TermTypes.METABOLITE_INTENSITY]: TermTypeGroups.METABOLITE_INTENSITY,
73
- [TermTypes.TERM_COLLECTION]: TermTypeGroups.TERM_COLLECTION
73
+ [TermTypes.TERM_COLLECTION]: TermTypeGroups.TERM_COLLECTION,
74
+ [TermTypes.SINGLECELL_CELLTYPE]: TermTypeGroups.SINGLECELL_CELLTYPE
74
75
  }
75
76
 
76
77
  const nonDictTypes = new Set([
@@ -196,6 +197,7 @@ export function getParentType(types, ds) {
196
197
  if (!ids || ids.length == 0) return null
197
198
  for (const id of ids) {
198
199
  const typeObj = ds.cohort.termdb.sampleTypes[id]
200
+ if (!typeObj) continue
199
201
  if (typeObj.parent_id == null) return id //this is the root type
200
202
  //if my parent is in the list, then I am not the parent
201
203
  if (ids.includes(typeObj.parent_id)) continue