axios-cache-interceptor
Advanced tools
Comparing version 0.4.0 to 0.4.1
@@ -6,86 +6,85 @@ "use strict"; | ||
class CacheRequestInterceptor { | ||
axios; | ||
constructor(axios) { | ||
this.axios = axios; | ||
} | ||
use = () => { | ||
this.axios.interceptors.request.use(this.onFulfilled); | ||
}; | ||
onFulfilled = async (config) => { | ||
// Skip cache | ||
if (config.cache === false) { | ||
return config; | ||
} | ||
// Only cache specified methods | ||
const allowedMethods = config.cache?.methods || this.axios.defaults.cache.methods; | ||
if (!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)) { | ||
return config; | ||
} | ||
const key = this.axios.generateKey(config); | ||
// Assumes that the storage handled staled responses | ||
let cache = await this.axios.storage.get(key); | ||
// Not cached, continue the request, and mark it as fetching | ||
emptyState: if (cache.state == 'empty') { | ||
/** | ||
* This checks for simultaneous access to a new key. The js | ||
* event loop jumps on the first await statement, so the second | ||
* (asynchronous call) request may have already started executing. | ||
*/ | ||
if (this.axios.waiting[key]) { | ||
cache = (await this.axios.storage.get(key)); | ||
break emptyState; | ||
this.use = () => { | ||
this.axios.interceptors.request.use(this.onFulfilled); | ||
}; | ||
this.onFulfilled = async (config) => { | ||
// Skip cache | ||
if (config.cache === false) { | ||
return config; | ||
} | ||
// Create a deferred to resolve other requests for the same key when it's completed | ||
this.axios.waiting[key] = (0, deferred_1.deferred)(); | ||
// Only cache specified methods | ||
const allowedMethods = config.cache?.methods || this.axios.defaults.cache.methods; | ||
if (!allowedMethods.some((method) => (config.method || 'get').toLowerCase() == method)) { | ||
return config; | ||
} | ||
const key = this.axios.generateKey(config); | ||
// Assumes that the storage handled staled responses | ||
let cache = await this.axios.storage.get(key); | ||
// Not cached, continue the request, and mark it as fetching | ||
emptyState: if (cache.state == 'empty') { | ||
/** | ||
* This checks for simultaneous access to a new key. The js | ||
* event loop jumps on the first await statement, so the second | ||
* (asynchronous call) request may have already started executing. | ||
*/ | ||
if (this.axios.waiting[key]) { | ||
cache = (await this.axios.storage.get(key)); | ||
break emptyState; | ||
} | ||
// Create a deferred to resolve other requests for the same key when it's completed | ||
this.axios.waiting[key] = (0, deferred_1.deferred)(); | ||
/** | ||
* Add a default reject handler to catch when the request is | ||
* aborted without others waiting for it. | ||
*/ | ||
this.axios.waiting[key]?.catch(() => undefined); | ||
await this.axios.storage.set(key, { | ||
state: 'loading', | ||
ttl: config.cache?.ttl | ||
}); | ||
return config; | ||
} | ||
let cachedResponse; | ||
if (cache.state === 'loading') { | ||
const deferred = this.axios.waiting[key]; | ||
/** | ||
* If the deferred is undefined, means that the outside has | ||
* removed that key from the waiting list | ||
*/ | ||
if (!deferred) { | ||
await this.axios.storage.remove(key); | ||
return config; | ||
} | ||
try { | ||
cachedResponse = await deferred; | ||
} | ||
catch (e) { | ||
// The deferred is rejected when the request that we are waiting rejected cache. | ||
return config; | ||
} | ||
} | ||
else { | ||
cachedResponse = cache.data; | ||
} | ||
config.adapter = () => | ||
/** | ||
* Add a default reject handler to catch when the request is | ||
* aborted without others waiting for it. | ||
* Even though the response interceptor receives this one from | ||
* here, it has been configured to ignore cached responses: true | ||
*/ | ||
this.axios.waiting[key]?.catch(() => undefined); | ||
await this.axios.storage.set(key, { | ||
state: 'loading', | ||
ttl: config.cache?.ttl | ||
Promise.resolve({ | ||
config: config, | ||
data: cachedResponse.data, | ||
headers: cachedResponse.headers, | ||
status: cachedResponse.status, | ||
statusText: cachedResponse.statusText, | ||
cached: true, | ||
id: key | ||
}); | ||
return config; | ||
} | ||
let cachedResponse; | ||
if (cache.state === 'loading') { | ||
const deferred = this.axios.waiting[key]; | ||
/** | ||
* If the deferred is undefined, means that the outside has | ||
* removed that key from the waiting list | ||
*/ | ||
if (!deferred) { | ||
await this.axios.storage.remove(key); | ||
return config; | ||
} | ||
try { | ||
cachedResponse = await deferred; | ||
} | ||
catch (e) { | ||
// The deferred is rejected when the request that we are waiting rejected cache. | ||
return config; | ||
} | ||
} | ||
else { | ||
cachedResponse = cache.data; | ||
} | ||
config.adapter = () => | ||
/** | ||
* Even though the response interceptor receives this one from | ||
* here, it has been configured to ignore cached responses: true | ||
*/ | ||
Promise.resolve({ | ||
config: config, | ||
data: cachedResponse.data, | ||
headers: cachedResponse.headers, | ||
status: cachedResponse.status, | ||
statusText: cachedResponse.statusText, | ||
cached: true, | ||
id: key | ||
}); | ||
return config; | ||
}; | ||
}; | ||
} | ||
} | ||
exports.CacheRequestInterceptor = CacheRequestInterceptor; | ||
//# sourceMappingURL=request.js.map |
@@ -8,88 +8,89 @@ "use strict"; | ||
class CacheResponseInterceptor { | ||
axios; | ||
constructor(axios) { | ||
this.axios = axios; | ||
} | ||
use = () => { | ||
this.axios.interceptors.response.use(this.onFulfilled); | ||
}; | ||
testCachePredicate = (response, cache) => { | ||
const cachePredicate = cache?.cachePredicate || this.axios.defaults.cache.cachePredicate; | ||
return ((typeof cachePredicate === 'function' && cachePredicate(response)) || | ||
(typeof cachePredicate === 'object' && | ||
(0, cache_predicate_1.checkPredicateObject)(response, cachePredicate))); | ||
}; | ||
/** | ||
* Rejects cache for this response. Also update the waiting list for | ||
* this key by rejecting it. | ||
*/ | ||
rejectResponse = async (key) => { | ||
// Update the cache to empty to prevent infinite loading state | ||
await this.axios.storage.remove(key); | ||
// Reject the deferred if present | ||
this.axios.waiting[key]?.reject(); | ||
delete this.axios.waiting[key]; | ||
}; | ||
onFulfilled = async (axiosResponse) => { | ||
const key = this.axios.generateKey(axiosResponse.config); | ||
const response = { | ||
id: key, | ||
// When the request interceptor override the request adapter, it means | ||
// that the response.cached will be true and therefore, the request was cached. | ||
cached: axiosResponse.cached || false, | ||
...axiosResponse | ||
this.use = () => { | ||
this.axios.interceptors.response.use(this.onFulfilled); | ||
}; | ||
// Skip cache | ||
if (response.config.cache === false) { | ||
return { ...response, cached: false }; | ||
} | ||
// Response was marked as cached | ||
if (response.cached) { | ||
return response; | ||
} | ||
const cache = await this.axios.storage.get(key); | ||
this.testCachePredicate = (response, cache) => { | ||
const cachePredicate = cache?.cachePredicate || this.axios.defaults.cache.cachePredicate; | ||
return ((typeof cachePredicate === 'function' && cachePredicate(response)) || | ||
(typeof cachePredicate === 'object' && | ||
(0, cache_predicate_1.checkPredicateObject)(response, cachePredicate))); | ||
}; | ||
/** | ||
* From now on, the cache and response represents the state of the | ||
* first response to a request, which has not yet been cached or | ||
* processed before. | ||
* Rejects cache for this response. Also update the waiting list for | ||
* this key by rejecting it. | ||
*/ | ||
if (cache.state !== 'loading') { | ||
return response; | ||
} | ||
// Config told that this response should be cached. | ||
if (!this.testCachePredicate(response, response.config.cache)) { | ||
await this.rejectResponse(key); | ||
return response; | ||
} | ||
let ttl = response.config.cache?.ttl || this.axios.defaults.cache.ttl; | ||
if (response.config.cache?.interpretHeader) { | ||
const expirationTime = this.axios.headerInterpreter(response.headers); | ||
// Cache should not be used | ||
if (expirationTime === false) { | ||
this.rejectResponse = async (key) => { | ||
// Update the cache to empty to prevent infinite loading state | ||
await this.axios.storage.remove(key); | ||
// Reject the deferred if present | ||
this.axios.waiting[key]?.reject(); | ||
delete this.axios.waiting[key]; | ||
}; | ||
this.onFulfilled = async (axiosResponse) => { | ||
const key = this.axios.generateKey(axiosResponse.config); | ||
const response = { | ||
id: key, | ||
/** | ||
* The request interceptor response.cache will return true or | ||
* undefined. And true only when the response was cached. | ||
*/ | ||
cached: axiosResponse.cached || false, | ||
...axiosResponse | ||
}; | ||
// Skip cache | ||
if (response.config.cache === false) { | ||
return { ...response, cached: false }; | ||
} | ||
// Response is already cached | ||
if (response.cached) { | ||
return response; | ||
} | ||
const cache = await this.axios.storage.get(key); | ||
/** | ||
* From now on, the cache and response represents the state of the | ||
* first response to a request, which has not yet been cached or | ||
* processed before. | ||
*/ | ||
if (cache.state !== 'loading') { | ||
return response; | ||
} | ||
// Config told that this response should be cached. | ||
if (!this.testCachePredicate(response, response.config.cache)) { | ||
await this.rejectResponse(key); | ||
return response; | ||
} | ||
ttl = expirationTime ? expirationTime : ttl; | ||
} | ||
const newCache = { | ||
state: 'cached', | ||
ttl: ttl, | ||
createdAt: Date.now(), | ||
data: (0, object_1.extract)(response, ['data', 'headers', 'status', 'statusText']) | ||
let ttl = response.config.cache?.ttl || this.axios.defaults.cache.ttl; | ||
if (response.config.cache?.interpretHeader) { | ||
const expirationTime = this.axios.headerInterpreter(response.headers); | ||
// Cache should not be used | ||
if (expirationTime === false) { | ||
await this.rejectResponse(key); | ||
return response; | ||
} | ||
ttl = expirationTime ? expirationTime : ttl; | ||
} | ||
const newCache = { | ||
state: 'cached', | ||
ttl: ttl, | ||
createdAt: Date.now(), | ||
data: (0, object_1.extract)(response, ['data', 'headers', 'status', 'statusText']) | ||
}; | ||
// Update other entries before updating himself | ||
if (response.config.cache?.update) { | ||
(0, update_cache_1.updateCache)(this.axios.storage, response.data, response.config.cache.update); | ||
} | ||
const deferred = this.axios.waiting[key]; | ||
// Resolve all other requests waiting for this response | ||
await deferred?.resolve(newCache.data); | ||
delete this.axios.waiting[key]; | ||
// Define this key as cache on the storage | ||
await this.axios.storage.set(key, newCache); | ||
// Return the response with cached as false, because it was not cached at all | ||
return response; | ||
}; | ||
// Update other entries before updating himself | ||
if (response.config.cache?.update) { | ||
(0, update_cache_1.updateCache)(this.axios.storage, response.data, response.config.cache.update); | ||
} | ||
const deferred = this.axios.waiting[key]; | ||
// Resolve all other requests waiting for this response | ||
await deferred?.resolve(newCache.data); | ||
delete this.axios.waiting[key]; | ||
// Define this key as cache on the storage | ||
await this.axios.storage.set(key, newCache); | ||
// Return the response with cached as false, because it was not cached at all | ||
return response; | ||
}; | ||
} | ||
} | ||
exports.CacheResponseInterceptor = CacheResponseInterceptor; | ||
//# sourceMappingURL=response.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.MemoryStorage = void 0; | ||
const util_1 = require("./util"); | ||
class MemoryStorage { | ||
storage = new Map(); | ||
get = async (key) => { | ||
const value = this.storage.get(key); | ||
if (!value) { | ||
return { state: 'empty' }; | ||
} | ||
if (value.state === 'cached' && value.createdAt + value.ttl < Date.now()) { | ||
this.remove(key); | ||
return { state: 'empty' }; | ||
} | ||
return value; | ||
}; | ||
set = async (key, value) => { | ||
this.storage.set(key, value); | ||
}; | ||
remove = async (key) => { | ||
this.storage.delete(key); | ||
}; | ||
constructor() { | ||
this.storage = new Map(); | ||
this.get = async (key) => { | ||
const value = this.storage.get(key); | ||
if (!value) { | ||
return { state: 'empty' }; | ||
} | ||
if ((0, util_1.isCacheValid)(value) === false) { | ||
this.remove(key); | ||
return { state: 'empty' }; | ||
} | ||
return value; | ||
}; | ||
this.set = async (key, value) => { | ||
this.storage.set(key, value); | ||
}; | ||
this.remove = async (key) => { | ||
this.storage.delete(key); | ||
}; | ||
} | ||
} | ||
exports.MemoryStorage = MemoryStorage; | ||
//# sourceMappingURL=memory.js.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.SessionCacheStorage = exports.LocalCacheStorage = exports.WindowStorageWrapper = void 0; | ||
exports.SessionCacheStorage = exports.LocalCacheStorage = exports.WindowStorageWrapper = exports.DEFAULT_KEY_PREFIX = void 0; | ||
const util_1 = require("./util"); | ||
/** | ||
* The key prefix used in WindowStorageWrapper to prevent key | ||
* collisions with other code. | ||
*/ | ||
exports.DEFAULT_KEY_PREFIX = 'axios-cache-interceptor'; | ||
/** | ||
* A storage that uses any {@link Storage} as his storage. | ||
* | ||
* **Note**: All storage keys used are prefixed with `prefix` value. | ||
*/ | ||
class WindowStorageWrapper { | ||
storage; | ||
prefix; | ||
constructor(storage, prefix = 'axios-cache:') { | ||
/** | ||
* Creates a new instance of WindowStorageWrapper | ||
* | ||
* @param storage The storage to interact | ||
* @param prefix The prefix to use for all keys or | ||
* `DEFAULT_KEY_PREFIX` if not provided. | ||
* @see DEFAULT_KEY_PREFIX | ||
*/ | ||
constructor(storage, prefix = exports.DEFAULT_KEY_PREFIX) { | ||
this.storage = storage; | ||
this.prefix = prefix; | ||
this.get = async (key) => { | ||
const prefixedKey = this.prefixKey(key); | ||
const json = this.storage.getItem(prefixedKey); | ||
if (!json) { | ||
return { state: 'empty' }; | ||
} | ||
const parsed = JSON.parse(json); | ||
if ((0, util_1.isCacheValid)(parsed) === false) { | ||
this.storage.removeItem(prefixedKey); | ||
return { state: 'empty' }; | ||
} | ||
return parsed; | ||
}; | ||
this.set = async (key, value) => { | ||
const json = JSON.stringify(value); | ||
this.storage.setItem(this.prefixKey(key), json); | ||
}; | ||
this.remove = async (key) => { | ||
this.storage.removeItem(this.prefixKey(key)); | ||
}; | ||
this.prefixKey = (key) => `${this.prefix}:${key}`; | ||
} | ||
get = async (_key) => { | ||
const key = this.prefix + _key; | ||
const json = this.storage.getItem(key); | ||
if (!json) { | ||
return { state: 'empty' }; | ||
} | ||
const parsed = JSON.parse(json); | ||
if (parsed.state === 'cached' && parsed.createdAt + parsed.ttl < Date.now()) { | ||
this.storage.removeItem(key); | ||
return { state: 'empty' }; | ||
} | ||
return parsed; | ||
}; | ||
set = async (key, value) => { | ||
const json = JSON.stringify(value); | ||
this.storage.setItem(this.prefix + key, json); | ||
}; | ||
remove = async (key) => { | ||
this.storage.removeItem(this.prefix + key); | ||
}; | ||
} | ||
@@ -35,0 +50,0 @@ exports.WindowStorageWrapper = WindowStorageWrapper; |
{ | ||
"name": "axios-cache-interceptor", | ||
"version": "0.4.0", | ||
"version": "0.4.1", | ||
"description": "Cache interceptor for axios", | ||
"main": "dist/index.js", | ||
"main": "./dist/index.js", | ||
"types": "./types/index.d.ts", | ||
"typings": "./types/index.d.ts", | ||
"scripts": { | ||
@@ -44,3 +46,3 @@ "build": "tsc --p tsconfig.build.json", | ||
"devDependencies": { | ||
"@arthurfiorette/prettier-config": "^1.0.6", | ||
"@arthurfiorette/prettier-config": "*", | ||
"@types/jest": "^27.0.2", | ||
@@ -47,0 +49,0 @@ "@types/node": "^16.7.10", |
@@ -52,4 +52,7 @@ import type { AxiosResponse } from 'axios'; | ||
id: key, | ||
// When the request interceptor override the request adapter, it means | ||
// that the response.cached will be true and therefore, the request was cached. | ||
/** | ||
* The request interceptor response.cache will return true or | ||
* undefined. And true only when the response was cached. | ||
*/ | ||
cached: (axiosResponse as CacheAxiosResponse<R, D>).cached || false, | ||
@@ -64,3 +67,3 @@ ...axiosResponse | ||
// Response was marked as cached | ||
// Response is already cached | ||
if (response.cached) { | ||
@@ -67,0 +70,0 @@ return response; |
import type { CacheStorage, StorageValue } from './types'; | ||
import { isCacheValid } from './util'; | ||
@@ -13,3 +14,3 @@ export class MemoryStorage implements CacheStorage { | ||
if (value.state === 'cached' && value.createdAt + value.ttl < Date.now()) { | ||
if (isCacheValid(value) === false) { | ||
this.remove(key); | ||
@@ -16,0 +17,0 @@ return { state: 'empty' }; |
@@ -35,2 +35,6 @@ export interface CacheStorage { | ||
data: CachedResponse; | ||
/** | ||
* The number in milliseconds to wait after createdAt before the | ||
* value is considered stale. | ||
*/ | ||
ttl: number; | ||
@@ -37,0 +41,0 @@ createdAt: number; |
import type { CacheStorage, StorageValue } from './types'; | ||
import { isCacheValid } from './util'; | ||
/** | ||
* The key prefix used in WindowStorageWrapper to prevent key | ||
* collisions with other code. | ||
*/ | ||
export const DEFAULT_KEY_PREFIX = 'axios-cache-interceptor'; | ||
/** | ||
* A storage that uses any {@link Storage} as his storage. | ||
* | ||
* **Note**: All storage keys used are prefixed with `prefix` value. | ||
*/ | ||
export abstract class WindowStorageWrapper implements CacheStorage { | ||
constructor(readonly storage: Storage, readonly prefix: string = 'axios-cache:') {} | ||
/** | ||
* Creates a new instance of WindowStorageWrapper | ||
* | ||
* @param storage The storage to interact | ||
* @param prefix The prefix to use for all keys or | ||
* `DEFAULT_KEY_PREFIX` if not provided. | ||
* @see DEFAULT_KEY_PREFIX | ||
*/ | ||
constructor(readonly storage: Storage, readonly prefix: string = DEFAULT_KEY_PREFIX) {} | ||
get = async (_key: string): Promise<StorageValue> => { | ||
const key = this.prefix + _key; | ||
const json = this.storage.getItem(key); | ||
get = async (key: string): Promise<StorageValue> => { | ||
const prefixedKey = this.prefixKey(key); | ||
const json = this.storage.getItem(prefixedKey); | ||
@@ -18,4 +36,4 @@ if (!json) { | ||
if (parsed.state === 'cached' && parsed.createdAt + parsed.ttl < Date.now()) { | ||
this.storage.removeItem(key); | ||
if (isCacheValid(parsed) === false) { | ||
this.storage.removeItem(prefixedKey); | ||
return { state: 'empty' }; | ||
@@ -29,8 +47,10 @@ } | ||
const json = JSON.stringify(value); | ||
this.storage.setItem(this.prefix + key, json); | ||
this.storage.setItem(this.prefixKey(key), json); | ||
}; | ||
remove = async (key: string): Promise<void> => { | ||
this.storage.removeItem(this.prefix + key); | ||
this.storage.removeItem(this.prefixKey(key)); | ||
}; | ||
private prefixKey = (key: string): string => `${this.prefix}:${key}`; | ||
} | ||
@@ -37,0 +57,0 @@ |
@@ -14,3 +14,3 @@ { | ||
/* Language and Environment */ | ||
"target": "ESNext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, | ||
"target": "ES2021" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, | ||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */ | ||
@@ -30,3 +30,3 @@ // "jsx": "preserve", /* Specify what JSX code is generated. */ | ||
// "rootDir": "./", /* Specify the root folder within your source files. */ | ||
// "moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ | ||
"moduleResolution": "node", /* Specify how TypeScript looks up a file from a given module specifier. */ | ||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */ | ||
@@ -38,3 +38,3 @@ // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */ | ||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ | ||
// "resolveJsonModule": true, /* Enable importing .json files */ | ||
"resolveJsonModule": false, /* Enable importing .json files */ | ||
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */ | ||
@@ -69,9 +69,9 @@ | ||
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */ | ||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */ | ||
"declarationDir": "./types", /* Specify the output directory for generated declaration files. */ | ||
/* Interop Constraints */ | ||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ | ||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ | ||
"isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */ | ||
"allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */ | ||
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */, | ||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ | ||
"preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */ | ||
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */, | ||
@@ -81,3 +81,3 @@ | ||
"strict": true /* Enable all strict type-checking options. */, | ||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ | ||
"noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */ | ||
"strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */, | ||
@@ -102,5 +102,5 @@ "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */, | ||
/* Completeness */ | ||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ | ||
"skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */ | ||
"skipLibCheck": true /* Skip type checking all .d.ts files. */ | ||
} | ||
} |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
137558
94
1839