@prairielearn/cache 2.0.23 → 2.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/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # @prairielearn/cache
2
2
 
3
+ ## 2.1.0
4
+
5
+ ### Minor Changes
6
+
7
+ - e1c7f75: Add support for an optional type parameter to `cache.get(...)`
8
+
9
+ ## 2.0.24
10
+
11
+ ### Patch Changes
12
+
13
+ - 2f5ce1f: Upgrade all JavaScript dependencies
14
+ - Updated dependencies [2f5ce1f]
15
+ - @prairielearn/sentry@4.0.4
16
+
3
17
  ## 2.0.23
4
18
 
5
19
  ### Patch Changes
package/dist/index.d.ts CHANGED
@@ -16,7 +16,7 @@ export declare class Cache {
16
16
  /**
17
17
  * Returns the value for the corresponding key if it exists in the cache; null otherwise.
18
18
  */
19
- get(key: string): Promise<any>;
19
+ get<T>(key: string): Promise<T | null>;
20
20
  /**
21
21
  * Clear all entries from the cache.
22
22
  */
package/dist/index.js CHANGED
@@ -95,7 +95,7 @@ export class Cache {
95
95
  if (typeof value === 'string') {
96
96
  return JSON.parse(value);
97
97
  }
98
- return undefined;
98
+ return null;
99
99
  }
100
100
  case 'redis': {
101
101
  assert(this.redisClient, 'Redis client is enabled but not configured');
@@ -103,7 +103,7 @@ export class Cache {
103
103
  if (typeof value === 'string') {
104
104
  return JSON.parse(value);
105
105
  }
106
- return undefined;
106
+ return null;
107
107
  }
108
108
  default: {
109
109
  return null;
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from 'node:assert';\n\nimport { Redis } from 'ioredis';\nimport { LRUCache } from 'lru-cache';\n\nimport { logger } from '@prairielearn/logger';\nimport * as Sentry from '@prairielearn/sentry';\n\nexport class Cache {\n enabled = false;\n type = 'none';\n memoryCache?: LRUCache<string, string>;\n redisClient?: Redis;\n keyPrefix = '';\n\n async init(config: {\n type: 'none' | 'memory' | 'redis';\n keyPrefix: string;\n redisUrl?: string | null;\n }) {\n this.type = config.type;\n this.keyPrefix = config.keyPrefix;\n if (!this.type || this.type === 'none') {\n // No caching\n this.enabled = false;\n return;\n }\n\n if (this.type === 'redis') {\n if (!config.redisUrl) throw new Error('redisUrl not set in config');\n this.enabled = true;\n this.redisClient = new Redis(config.redisUrl);\n this.redisClient.on('error', (err) => {\n logger.error('Redis error', err);\n Sentry.captureException(err);\n });\n } else if (this.type === 'memory') {\n this.enabled = true;\n this.memoryCache = new LRUCache({\n // The in-memory cache is really only suited for development, so we'll\n // hardcode a relatively low limit here.\n max: 1000,\n });\n } else {\n throw new Error(`Unknown cache type \"${this.type}\"`);\n }\n }\n\n set(key: string, value: any, maxAgeMS: number) {\n if (!this.enabled) return;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.set(scopedKey, JSON.stringify(value), { ttl: maxAgeMS });\n break;\n }\n\n case 'redis': {\n // This returns a promise, but we don't want to wait for this data\n // to reach the cache before continuing, and we don't *really*\n // care if it errors.\n //\n // We don't log the error because it contains the cached value,\n // which can be huge and which fills up the logs.\n assert(this.redisClient, 'Redis client is enabled but not configured');\n this.redisClient\n .set(scopedKey, JSON.stringify(value), 'PX', maxAgeMS)\n .catch((_err) => logger.error('Cache set error', { key, scopedKey, maxAgeMS }));\n break;\n }\n }\n }\n\n async del(key: string) {\n if (!this.enabled) return;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.delete(scopedKey);\n break;\n }\n\n case 'redis': {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.del(scopedKey);\n break;\n }\n }\n }\n\n /**\n * Returns the value for the corresponding key if it exists in the cache; null otherwise.\n */\n async get(key: string): Promise<any> {\n if (!this.enabled) return null;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n const value = this.memoryCache.get(scopedKey);\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return undefined;\n }\n\n case 'redis': {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n const value = await this.redisClient.get(scopedKey);\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return undefined;\n }\n\n default: {\n return null;\n }\n }\n }\n\n /**\n * Clear all entries from the cache.\n */\n async reset() {\n if (!this.enabled) return;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.clear();\n break;\n }\n\n case 'redis': {\n let cursor = '0';\n do {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n const reply = await this.redisClient.scan(\n cursor,\n 'MATCH',\n `${this.keyPrefix}*`,\n 'COUNT',\n 1000,\n );\n cursor = reply[0];\n\n const keys = reply[1];\n if (keys.length > 0) {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.del(keys);\n }\n } while (cursor !== '0');\n break;\n }\n }\n }\n\n /**\n * Releases any connections associated with the cache.\n */\n async close() {\n if (!this.enabled) return;\n this.enabled = false;\n\n if (this.type === 'redis') {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.quit();\n }\n }\n}\n\nexport const cache = new Cache();\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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","sourcesContent":["import assert from 'node:assert';\n\nimport { Redis } from 'ioredis';\nimport { LRUCache } from 'lru-cache';\n\nimport { logger } from '@prairielearn/logger';\nimport * as Sentry from '@prairielearn/sentry';\n\nexport class Cache {\n enabled = false;\n type = 'none';\n memoryCache?: LRUCache<string, string>;\n redisClient?: Redis;\n keyPrefix = '';\n\n async init(config: {\n type: 'none' | 'memory' | 'redis';\n keyPrefix: string;\n redisUrl?: string | null;\n }) {\n this.type = config.type;\n this.keyPrefix = config.keyPrefix;\n if (!this.type || this.type === 'none') {\n // No caching\n this.enabled = false;\n return;\n }\n\n if (this.type === 'redis') {\n if (!config.redisUrl) throw new Error('redisUrl not set in config');\n this.enabled = true;\n this.redisClient = new Redis(config.redisUrl);\n this.redisClient.on('error', (err) => {\n logger.error('Redis error', err);\n Sentry.captureException(err);\n });\n } else if (this.type === 'memory') {\n this.enabled = true;\n this.memoryCache = new LRUCache({\n // The in-memory cache is really only suited for development, so we'll\n // hardcode a relatively low limit here.\n max: 1000,\n });\n } else {\n throw new Error(`Unknown cache type \"${this.type}\"`);\n }\n }\n\n set(key: string, value: any, maxAgeMS: number) {\n if (!this.enabled) return;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.set(scopedKey, JSON.stringify(value), { ttl: maxAgeMS });\n break;\n }\n\n case 'redis': {\n // This returns a promise, but we don't want to wait for this data\n // to reach the cache before continuing, and we don't *really*\n // care if it errors.\n //\n // We don't log the error because it contains the cached value,\n // which can be huge and which fills up the logs.\n assert(this.redisClient, 'Redis client is enabled but not configured');\n this.redisClient\n .set(scopedKey, JSON.stringify(value), 'PX', maxAgeMS)\n .catch((_err) => logger.error('Cache set error', { key, scopedKey, maxAgeMS }));\n break;\n }\n }\n }\n\n async del(key: string) {\n if (!this.enabled) return;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.delete(scopedKey);\n break;\n }\n\n case 'redis': {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.del(scopedKey);\n break;\n }\n }\n }\n\n /**\n * Returns the value for the corresponding key if it exists in the cache; null otherwise.\n */\n async get<T>(key: string): Promise<T | null> {\n if (!this.enabled) return null;\n\n const scopedKey = this.keyPrefix + key;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n const value = this.memoryCache.get(scopedKey);\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return null;\n }\n\n case 'redis': {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n const value = await this.redisClient.get(scopedKey);\n if (typeof value === 'string') {\n return JSON.parse(value);\n }\n return null;\n }\n\n default: {\n return null;\n }\n }\n }\n\n /**\n * Clear all entries from the cache.\n */\n async reset() {\n if (!this.enabled) return;\n\n switch (this.type) {\n case 'memory': {\n assert(this.memoryCache, 'Memory cache is enabled but not configured');\n this.memoryCache.clear();\n break;\n }\n\n case 'redis': {\n let cursor = '0';\n do {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n const reply = await this.redisClient.scan(\n cursor,\n 'MATCH',\n `${this.keyPrefix}*`,\n 'COUNT',\n 1000,\n );\n cursor = reply[0];\n\n const keys = reply[1];\n if (keys.length > 0) {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.del(keys);\n }\n } while (cursor !== '0');\n break;\n }\n }\n }\n\n /**\n * Releases any connections associated with the cache.\n */\n async close() {\n if (!this.enabled) return;\n this.enabled = false;\n\n if (this.type === 'redis') {\n assert(this.redisClient, 'Redis client is enabled but not configured');\n await this.redisClient.quit();\n }\n }\n}\n\nexport const cache = new Cache();\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@prairielearn/cache",
3
- "version": "2.0.23",
3
+ "version": "2.1.0",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -9,9 +9,9 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@prairielearn/logger": "^2.0.20",
12
- "@prairielearn/sentry": "^4.0.3",
12
+ "@prairielearn/sentry": "^4.0.4",
13
13
  "ioredis": "^5.7.0",
14
- "lru-cache": "^11.1.0",
14
+ "lru-cache": "^11.2.0",
15
15
  "zod": "^3.25.76"
16
16
  },
17
17
  "devDependencies": {
package/src/index.ts CHANGED
@@ -97,7 +97,7 @@ export class Cache {
97
97
  /**
98
98
  * Returns the value for the corresponding key if it exists in the cache; null otherwise.
99
99
  */
100
- async get(key: string): Promise<any> {
100
+ async get<T>(key: string): Promise<T | null> {
101
101
  if (!this.enabled) return null;
102
102
 
103
103
  const scopedKey = this.keyPrefix + key;
@@ -109,7 +109,7 @@ export class Cache {
109
109
  if (typeof value === 'string') {
110
110
  return JSON.parse(value);
111
111
  }
112
- return undefined;
112
+ return null;
113
113
  }
114
114
 
115
115
  case 'redis': {
@@ -118,7 +118,7 @@ export class Cache {
118
118
  if (typeof value === 'string') {
119
119
  return JSON.parse(value);
120
120
  }
121
- return undefined;
121
+ return null;
122
122
  }
123
123
 
124
124
  default: {