@autofleet/matmon 2.3.0 → 2.3.1-beta-1

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/lib/cache.d.ts ADDED
@@ -0,0 +1,25 @@
1
+ import LRU from 'lru-cache';
2
+ interface Options {
3
+ lifeTimeInSec: number;
4
+ size?: number;
5
+ }
6
+ export declare const getNewLRU: <K = unknown, V = unknown>(lifeTimeInSec: Options["lifeTimeInSec"], size?: Options["size"]) => LRU<K, V>;
7
+ interface GetWithCacheOptions<V = unknown> {
8
+ cacheKey: string;
9
+ cacheGet: () => Promise<V>;
10
+ cacheSet: (value: V) => Promise<void>;
11
+ fetching: () => Promise<V>;
12
+ skipCache?: boolean;
13
+ }
14
+ export declare const getWithCacheSupport: <V = unknown>({ cacheKey, cacheGet, cacheSet, fetching, skipCache, }: GetWithCacheOptions<V>) => Promise<any>;
15
+ interface GetMultipleWithCacheOptions<V = unknown> {
16
+ getFromCache?: (query: any) => Promise<V>;
17
+ multiGetterFromCache?: (queries: any[]) => Promise<V[]>;
18
+ setInCache: (key: string, value: V) => Promise<void>;
19
+ getter?: (query: any) => Promise<V>;
20
+ multiGetter?: (queries: any[]) => Promise<V[]>;
21
+ setMultiInCache?: (keyValues: Record<string, object>) => Promise<void>;
22
+ idField?: string;
23
+ }
24
+ export declare const getMultipleWithCache: <V = unknown>({ getFromCache, multiGetterFromCache, setInCache, setMultiInCache, getter, multiGetter, idField, }: GetMultipleWithCacheOptions<V>) => (queries: any[]) => Promise<V[]>;
25
+ export {};
package/lib/cache.js ADDED
@@ -0,0 +1,140 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || function (mod) {
19
+ if (mod && mod.__esModule) return mod;
20
+ var result = {};
21
+ if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
22
+ __setModuleDefault(result, mod);
23
+ return result;
24
+ };
25
+ var __importDefault = (this && this.__importDefault) || function (mod) {
26
+ return (mod && mod.__esModule) ? mod : { "default": mod };
27
+ };
28
+ Object.defineProperty(exports, "__esModule", { value: true });
29
+ exports.getMultipleWithCache = exports.getWithCacheSupport = exports.getNewLRU = void 0;
30
+ const lru_cache_1 = __importDefault(require("lru-cache"));
31
+ const locking = __importStar(require("./locking"));
32
+ const dotenv_1 = __importDefault(require("dotenv"));
33
+ const logger_1 = __importDefault(require("./logger"));
34
+ dotenv_1.default.config();
35
+ const DEFAULT_CACHE_SIZE = 300;
36
+ const MAX_SIZE = 100000;
37
+ const MUTEX_MAP = {};
38
+ const getOptions = ({ lifeTimeInSec, size = DEFAULT_CACHE_SIZE, }) => ({
39
+ max: Math.min(size, MAX_SIZE),
40
+ maxAge: process.env.NODE_ENV !== 'test' ? 1000 * lifeTimeInSec : 0,
41
+ });
42
+ const getMutexByCacheKey = (key) => {
43
+ MUTEX_MAP[key] || (MUTEX_MAP[key] = locking.getMutex());
44
+ return MUTEX_MAP[key];
45
+ };
46
+ const deleteMutexByCacheKey = (key) => {
47
+ if (MUTEX_MAP[key]) {
48
+ delete MUTEX_MAP[key];
49
+ }
50
+ };
51
+ const getNewLRU = (lifeTimeInSec, size) => new lru_cache_1.default(getOptions({
52
+ lifeTimeInSec,
53
+ size,
54
+ }));
55
+ exports.getNewLRU = getNewLRU;
56
+ const IN_LOCAL_TEST = process.env.IS_IN_MATMON_TESTING === 'true';
57
+ const IS_IN_SERVICE_TEST = process.env.NODE_ENV === 'test' && !IN_LOCAL_TEST;
58
+ const tryToSetInCache = async (callback) => {
59
+ try {
60
+ await callback();
61
+ }
62
+ catch (e) {
63
+ logger_1.default.error('Failed to set in cache', e);
64
+ }
65
+ };
66
+ const getWithCacheSupport = async ({ cacheKey, cacheGet, cacheSet, fetching, skipCache, }) => {
67
+ if (skipCache || IS_IN_SERVICE_TEST) {
68
+ const res = await fetching();
69
+ await tryToSetInCache(() => cacheSet(res));
70
+ return res;
71
+ }
72
+ let valueToReturn = null;
73
+ try {
74
+ await locking.wrapWithMutex(getMutexByCacheKey(cacheKey), async () => {
75
+ valueToReturn = await cacheGet();
76
+ if (!valueToReturn) {
77
+ valueToReturn = await fetching();
78
+ await tryToSetInCache(() => cacheSet(valueToReturn));
79
+ }
80
+ else {
81
+ // logger.info('get value from cache');
82
+ }
83
+ deleteMutexByCacheKey(cacheKey);
84
+ });
85
+ // retry without locking if failed
86
+ }
87
+ catch (e) {
88
+ valueToReturn = await fetching();
89
+ await tryToSetInCache(() => cacheSet(valueToReturn));
90
+ deleteMutexByCacheKey(cacheKey);
91
+ }
92
+ return valueToReturn;
93
+ };
94
+ exports.getWithCacheSupport = getWithCacheSupport;
95
+ const getIdField = (query, idField) => {
96
+ if (typeof query === 'string') {
97
+ return query;
98
+ }
99
+ return query[idField];
100
+ };
101
+ const getMultipleWithCache = ({ getFromCache, multiGetterFromCache, setInCache, setMultiInCache, getter, multiGetter, idField = 'id', }) => async (queries) => {
102
+ const queriesMap = new Map(queries.filter(Boolean).map(query => [getIdField(query, idField), query]));
103
+ const resultMap = new Map();
104
+ const valuesToPullFromCache = [...queriesMap.values()];
105
+ const valuesFromCache = await (multiGetterFromCache?.(valuesToPullFromCache) || // Use multiGetterFromCache if it's provided
106
+ Promise.all(valuesToPullFromCache.map(query => getFromCache(query))) // Otherwise, iterate over the queries with getFromCache
107
+ );
108
+ valuesFromCache.filter(Boolean).forEach((value) => {
109
+ queriesMap.delete(getIdField(value, idField));
110
+ resultMap.set(getIdField(value, idField), value);
111
+ });
112
+ if (queriesMap.size > 0) {
113
+ const valuesFromGetter = await (multiGetter?.([...queriesMap.values()]) || // Use multiGetter if it's provided
114
+ Promise.all([...queriesMap.values()].map(id => getter(id))) // Otherwise, iterate over the queries with getter
115
+ );
116
+ if (setMultiInCache) {
117
+ const setCacheObject = valuesFromGetter.reduce((acc, value) => {
118
+ acc[getIdField(value, idField)] = value;
119
+ return acc;
120
+ }, {});
121
+ await tryToSetInCache(() => setMultiInCache(setCacheObject));
122
+ }
123
+ valuesFromGetter.forEach((value) => {
124
+ if (!value) {
125
+ return;
126
+ }
127
+ if (!setMultiInCache) {
128
+ tryToSetInCache(() => setInCache(value[idField], value));
129
+ }
130
+ resultMap.set(getIdField(value, idField), value);
131
+ });
132
+ }
133
+ return queries.map((query) => {
134
+ if (!query) {
135
+ return null;
136
+ }
137
+ return resultMap.get(getIdField(query, idField));
138
+ });
139
+ };
140
+ exports.getMultipleWithCache = getMultipleWithCache;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,172 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const cache_1 = require("./cache");
4
+ const async_mutex_1 = require("async-mutex");
5
+ jest.mock('./locking', () => ({
6
+ getMutex: jest.fn(() => new async_mutex_1.Mutex()),
7
+ wrapWithMutex: jest.fn((mutex, fn) => fn()),
8
+ }));
9
+ describe('Cache', () => {
10
+ describe('getNewLRU', () => {
11
+ it('should create a new LRU cache with default size', () => {
12
+ const cache = (0, cache_1.getNewLRU)(60);
13
+ expect(cache.max).toBe(300);
14
+ });
15
+ it('should create a new LRU cache with specified size', () => {
16
+ const cache = (0, cache_1.getNewLRU)(60, 500);
17
+ expect(cache.max).toBe(500);
18
+ });
19
+ it('should not exceed the maximum size', () => {
20
+ const cache = (0, cache_1.getNewLRU)(60, 200000);
21
+ expect(cache.max).toBe(100000);
22
+ });
23
+ });
24
+ describe('getWithCacheSupport', () => {
25
+ const cacheKey = 'testKey';
26
+ const createMocks = () => {
27
+ const cacheGet = jest.fn();
28
+ const cacheSet = jest.fn();
29
+ const fetching = jest.fn();
30
+ const getFromCache = jest.fn();
31
+ const setInCache = jest.fn();
32
+ const multiGetterFromCache = jest.fn();
33
+ const setMultiInCache = jest.fn();
34
+ return {
35
+ cacheGet,
36
+ cacheSet,
37
+ fetching,
38
+ getFromCache,
39
+ setInCache,
40
+ multiGetterFromCache,
41
+ setMultiInCache
42
+ };
43
+ };
44
+ beforeEach(() => {
45
+ jest.clearAllMocks();
46
+ });
47
+ it('should fetch and set value if skipCache is true', async () => {
48
+ const { cacheGet, cacheSet, fetching } = createMocks();
49
+ fetching.mockResolvedValue('fetchedValue');
50
+ await (0, cache_1.getWithCacheSupport)({ cacheKey, cacheGet, cacheSet, fetching, skipCache: true });
51
+ expect(fetching).toHaveBeenCalled();
52
+ expect(cacheSet).toHaveBeenCalledWith('fetchedValue');
53
+ });
54
+ it('should fetch and set value if cache is empty', async () => {
55
+ const { cacheGet, cacheSet, fetching } = createMocks();
56
+ cacheGet.mockResolvedValue(null);
57
+ fetching.mockResolvedValue('fetchedValue');
58
+ await (0, cache_1.getWithCacheSupport)({ cacheKey, cacheGet, cacheSet, fetching });
59
+ expect(fetching).toHaveBeenCalled();
60
+ expect(cacheSet).toHaveBeenCalledWith('fetchedValue');
61
+ });
62
+ it('should return cached value if available', async () => {
63
+ const { cacheGet, cacheSet, fetching } = createMocks();
64
+ cacheGet.mockResolvedValue('cachedValue');
65
+ const result = await (0, cache_1.getWithCacheSupport)({ cacheKey, cacheGet, cacheSet, fetching });
66
+ expect(result).toBe('cachedValue');
67
+ expect(fetching).not.toHaveBeenCalled();
68
+ });
69
+ it('should retry fetching if an error occurs', async () => {
70
+ const { cacheGet, cacheSet, fetching } = createMocks();
71
+ cacheGet.mockRejectedValue(new Error('Cache error'));
72
+ fetching.mockResolvedValue('fetchedValue');
73
+ const result = await (0, cache_1.getWithCacheSupport)({ cacheKey, cacheGet, cacheSet, fetching });
74
+ expect(result).toBe('fetchedValue');
75
+ expect(fetching).toHaveBeenCalled();
76
+ expect(cacheSet).toHaveBeenCalledWith('fetchedValue');
77
+ });
78
+ it('should not throw an error if cacheSet fails', async () => {
79
+ const { cacheGet, cacheSet, fetching } = createMocks();
80
+ cacheGet.mockResolvedValue(null);
81
+ cacheSet.mockRejectedValue(new Error('Cache set error'));
82
+ fetching.mockResolvedValue('fetchedValue');
83
+ await expect((0, cache_1.getWithCacheSupport)({ cacheKey, cacheGet, cacheSet, fetching })).resolves.not.toThrow();
84
+ });
85
+ });
86
+ describe('getMultipleWithCache', () => {
87
+ const buildMocks = () => {
88
+ const getFromCache = jest.fn();
89
+ const multiGetterFromCache = jest.fn();
90
+ const setInCache = jest.fn();
91
+ const setMultiInCache = jest.fn();
92
+ const getter = jest.fn();
93
+ const multiGetter = jest.fn();
94
+ return {
95
+ getFromCache,
96
+ multiGetterFromCache,
97
+ setInCache,
98
+ setMultiInCache,
99
+ getter,
100
+ multiGetter,
101
+ };
102
+ };
103
+ beforeEach(() => {
104
+ jest.clearAllMocks();
105
+ });
106
+ it('should return values from cache if available', async () => {
107
+ const { getFromCache, setInCache, getter } = buildMocks();
108
+ const queries = [{ id: 1 }, { id: 2 }];
109
+ getFromCache.mockResolvedValueOnce({ id: 1, value: 'cachedValue1' }).mockResolvedValueOnce({ id: 2, value: 'cachedValue2' });
110
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, getter });
111
+ const result = await getMultiple(queries);
112
+ expect(result.map(({ value }) => value)).toEqual(['cachedValue1', 'cachedValue2']);
113
+ });
114
+ it('should fetch and cache values if not in cache', async () => {
115
+ const { getFromCache, setInCache, getter } = buildMocks();
116
+ const queries = [{ id: 1 }, { id: 2 }];
117
+ getFromCache.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
118
+ getter.mockResolvedValueOnce({ id: 1, value: 'fetchedValue1' }).mockResolvedValueOnce({ id: 2, value: 'fetchedValue2' });
119
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, getter });
120
+ const result = await getMultiple(queries);
121
+ expect(result.map(({ value }) => value)).toEqual(['fetchedValue1', 'fetchedValue2']);
122
+ expect(setInCache).toHaveBeenCalledWith(1, { id: 1, value: 'fetchedValue1' });
123
+ expect(setInCache).toHaveBeenCalledWith(2, { id: 2, value: 'fetchedValue2' });
124
+ });
125
+ it('should use multiGetterFromCache if provided', async () => {
126
+ const { multiGetterFromCache, setInCache, getter } = buildMocks();
127
+ const queries = [{ id: 1 }, { id: 2 }];
128
+ multiGetterFromCache.mockResolvedValue([{ id: 1, value: 'cachedValue1' }, { id: 2, value: 'cachedValue2' }]);
129
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ multiGetterFromCache, setInCache, getter });
130
+ const result = await getMultiple(queries);
131
+ expect(result.map(({ value }) => value)).toEqual(['cachedValue1', 'cachedValue2']);
132
+ });
133
+ it('should use multiGetter if provided', async () => {
134
+ const { getFromCache, setInCache, multiGetter } = buildMocks();
135
+ const queries = [{ id: 1 }, { id: 2 }];
136
+ getFromCache.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
137
+ multiGetter.mockResolvedValue([{ id: 1, value: 'fetchedValue1' }, { id: 2, value: 'fetchedValue2' }]);
138
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, multiGetter });
139
+ const result = await getMultiple(queries);
140
+ expect(result.map(({ value }) => value)).toEqual(['fetchedValue1', 'fetchedValue2']);
141
+ expect(setInCache).toHaveBeenCalledWith(1, { id: 1, value: 'fetchedValue1' });
142
+ expect(setInCache).toHaveBeenCalledWith(2, { id: 2, value: 'fetchedValue2' });
143
+ });
144
+ it('should use setMultiInCache if provided', async () => {
145
+ const { getFromCache, setInCache, multiGetter, setMultiInCache } = buildMocks();
146
+ const queries = [{ id: 1 }, { id: 2 }];
147
+ getFromCache.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
148
+ multiGetter.mockResolvedValue([{ id: 1, value: 'fetchedValue1' }, { id: 2, value: 'fetchedValue2' }]);
149
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, multiGetter, setMultiInCache });
150
+ await getMultiple(queries);
151
+ expect(setMultiInCache).toHaveBeenCalledWith({ 1: { id: 1, value: 'fetchedValue1' }, 2: { id: 2, value: 'fetchedValue2' } });
152
+ });
153
+ it('Should not throw and error if setMultiInCache throws an error', async () => {
154
+ const { getFromCache, setInCache, multiGetter, setMultiInCache } = buildMocks();
155
+ const queries = [{ id: 1 }, { id: 2 }];
156
+ getFromCache.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
157
+ multiGetter.mockResolvedValue([{ id: 1, value: 'fetchedValue1' }, { id: 2, value: 'fetchedValue2' }]);
158
+ setMultiInCache.mockRejectedValue(new Error('Error setting cache'));
159
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, multiGetter, setMultiInCache });
160
+ await expect(getMultiple(queries)).resolves.not.toThrow();
161
+ });
162
+ it('should not throw an error if setInCache fails', async () => {
163
+ const { getFromCache, setInCache, getter } = buildMocks();
164
+ const queries = [{ id: 1 }, { id: 2 }];
165
+ getFromCache.mockResolvedValueOnce(null).mockResolvedValueOnce(null);
166
+ getter.mockResolvedValueOnce({ id: 1, value: 'fetchedValue1' }).mockResolvedValueOnce({ id: 2, value: 'fetchedValue2' });
167
+ setInCache.mockRejectedValue(new Error('Error setting cache'));
168
+ const getMultiple = (0, cache_1.getMultipleWithCache)({ getFromCache, setInCache, getter });
169
+ await expect(getMultiple(queries)).resolves.not.toThrow();
170
+ });
171
+ });
172
+ });
package/lib/index.d.ts ADDED
@@ -0,0 +1,4 @@
1
+ import { getNewLRU, getWithCacheSupport, getMultipleWithCache } from './cache';
2
+ import RedisCache from './redis';
3
+ import { ORMCache, ORMTypes } from './orm-cache';
4
+ export { getNewLRU, getWithCacheSupport, getMultipleWithCache, RedisCache, ORMCache, ORMTypes };
package/lib/index.js ADDED
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ORMTypes = exports.ORMCache = exports.RedisCache = exports.getMultipleWithCache = exports.getWithCacheSupport = exports.getNewLRU = void 0;
7
+ const cache_1 = require("./cache");
8
+ Object.defineProperty(exports, "getNewLRU", { enumerable: true, get: function () { return cache_1.getNewLRU; } });
9
+ Object.defineProperty(exports, "getWithCacheSupport", { enumerable: true, get: function () { return cache_1.getWithCacheSupport; } });
10
+ Object.defineProperty(exports, "getMultipleWithCache", { enumerable: true, get: function () { return cache_1.getMultipleWithCache; } });
11
+ const redis_1 = __importDefault(require("./redis"));
12
+ exports.RedisCache = redis_1.default;
13
+ const orm_cache_1 = require("./orm-cache");
14
+ Object.defineProperty(exports, "ORMCache", { enumerable: true, get: function () { return orm_cache_1.ORMCache; } });
15
+ Object.defineProperty(exports, "ORMTypes", { enumerable: true, get: function () { return orm_cache_1.ORMTypes; } });
@@ -0,0 +1,3 @@
1
+ import { MutexInterface } from 'async-mutex';
2
+ export declare const wrapWithMutex: (mutex: MutexInterface, funcToRun: () => PromiseLike<void>) => Promise<void>;
3
+ export declare const getMutex: () => MutexInterface;
package/lib/locking.js ADDED
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getMutex = exports.wrapWithMutex = void 0;
4
+ const async_mutex_1 = require("async-mutex");
5
+ const CACHE_LOCK_TIMEOUT_MILIS = 3000;
6
+ const LOCK_TIMEOUT_MESSAGE = 'mutex - locking timeout';
7
+ const wrapWithMutex = async (mutex, funcToRun) => {
8
+ const release = await mutex.acquire();
9
+ try {
10
+ await funcToRun();
11
+ }
12
+ finally {
13
+ release();
14
+ }
15
+ };
16
+ exports.wrapWithMutex = wrapWithMutex;
17
+ const getMutex = () => (0, async_mutex_1.withTimeout)(new async_mutex_1.Mutex(), CACHE_LOCK_TIMEOUT_MILIS, new Error(LOCK_TIMEOUT_MESSAGE));
18
+ exports.getMutex = getMutex;
@@ -0,0 +1,2 @@
1
+ declare const logger: import("@autofleet/logger").LoggerInstanceManager;
2
+ export default logger;
package/lib/logger.js ADDED
@@ -0,0 +1,8 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const logger_1 = __importDefault(require("@autofleet/logger"));
7
+ const logger = (0, logger_1.default)();
8
+ exports.default = logger;
@@ -0,0 +1,17 @@
1
+ import type RedisCache from '../redis';
2
+ export interface ModelOptions {
3
+ name: string;
4
+ associations?: AssociationOptions[];
5
+ }
6
+ export interface AssociationOptions {
7
+ name: string;
8
+ alias: string;
9
+ accessKey?: string;
10
+ innerAssociation?: AssociationOptions;
11
+ }
12
+ export interface Adapter {
13
+ ormInstance: any;
14
+ getModel: (modelName: string) => any;
15
+ injectGetWithCacheFunction: (cache: RedisCache, modelOptions: ModelOptions) => void;
16
+ addInvalidationHooks: (cache: RedisCache, modelOptions: ModelOptions) => void;
17
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,2 @@
1
+ export declare class UnsupportedOrmTypeError extends Error {
2
+ }
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.UnsupportedOrmTypeError = void 0;
4
+ class UnsupportedOrmTypeError extends Error {
5
+ }
6
+ exports.UnsupportedOrmTypeError = UnsupportedOrmTypeError;
@@ -0,0 +1,12 @@
1
+ import { ModelOptions } from './adapter';
2
+ export declare enum ORMTypes {
3
+ SEQUELIZE = "sequelize"
4
+ }
5
+ interface ORMCacheOptions {
6
+ type: ORMTypes;
7
+ models: ModelOptions[];
8
+ ormInstance: any;
9
+ debug: boolean;
10
+ }
11
+ export declare const ORMCache: (options: ORMCacheOptions) => void;
12
+ export {};
@@ -0,0 +1,39 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ORMCache = exports.ORMTypes = void 0;
7
+ const sequelize_adapter_1 = __importDefault(require("./sequelize-adapter"));
8
+ const errors_1 = require("./errors");
9
+ const redis_1 = __importDefault(require("../redis"));
10
+ const logger_1 = __importDefault(require("../logger"));
11
+ var ORMTypes;
12
+ (function (ORMTypes) {
13
+ ORMTypes["SEQUELIZE"] = "sequelize";
14
+ })(ORMTypes || (exports.ORMTypes = ORMTypes = {}));
15
+ const ORMInstanceFactory = (options) => {
16
+ switch (options.type) {
17
+ case ORMTypes.SEQUELIZE: {
18
+ return new sequelize_adapter_1.default(options.ormInstance, options.debug);
19
+ }
20
+ default: {
21
+ throw new errors_1.UnsupportedOrmTypeError(`ORM type ${options.type} is unsupported at the moment`);
22
+ }
23
+ }
24
+ };
25
+ const ORMCache = (options) => {
26
+ const { models } = options;
27
+ logger_1.default.info('Starting ORM Cache', { options });
28
+ const adapter = ORMInstanceFactory(options);
29
+ const cache = new redis_1.default({
30
+ host: process.env.REDIS_HOST,
31
+ port: process.env.REDIS_PORT,
32
+ });
33
+ // eslint-disable-next-line array-callback-return
34
+ models.map((modelOptions) => {
35
+ adapter.addInvalidationHooks(cache, modelOptions);
36
+ adapter.injectGetWithCacheFunction(cache, modelOptions);
37
+ });
38
+ };
39
+ exports.ORMCache = ORMCache;
@@ -0,0 +1,13 @@
1
+ import { Sequelize } from 'sequelize';
2
+ import { Adapter, AssociationOptions, ModelOptions } from './adapter';
3
+ import RedisCache from '../redis';
4
+ export default class SequelizeAdapter implements Adapter {
5
+ ormInstance: Sequelize;
6
+ debugMode: boolean;
7
+ constructor(sequelize: Sequelize, debug: any);
8
+ getModel(modelName: string): any;
9
+ getModelDependencies(modelName: string): AssociationOptions[];
10
+ debug(message: any, payload: any): void;
11
+ injectGetWithCacheFunction(cache: RedisCache, modelOptions: ModelOptions): void;
12
+ addInvalidationHooks(cache: RedisCache, modelOptions: ModelOptions): void;
13
+ }
@@ -0,0 +1,156 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const logger_1 = __importDefault(require("../logger"));
7
+ const util_1 = require("util");
8
+ const { AF_SERVICE_NAME } = process.env;
9
+ const ORM_CACHE_PREFIX = 'ormCache';
10
+ const INVALIDATION_HOOKS = ['afterSave', 'afterUpdate', 'afterDestroy'];
11
+ const BULK_HOOKS = ['beforeBulkUpdate', 'beforeBulkDestroy'];
12
+ const generateInstanceKey = (name, id) => `${AF_SERVICE_NAME}:${ORM_CACHE_PREFIX}:${name}_${id}`;
13
+ const generateDependencyKey = (modelName, associationName, associationId) => `${AF_SERVICE_NAME}:${ORM_CACHE_PREFIX}:${modelName}_${associationName}_${associationId}_DEPENDENCIES`;
14
+ const getInstanceDependencyKeys = (modelOptions, instance) => {
15
+ const keys = modelOptions.associations
16
+ .filter((associationOptions) => instance[associationOptions.alias])
17
+ .map((associationOptions) => {
18
+ const accessKey = associationOptions.accessKey;
19
+ const depKeys = [];
20
+ if (Array.isArray(instance[associationOptions.alias])) {
21
+ instance[associationOptions.alias].map(associationInstance => {
22
+ depKeys.push(generateDependencyKey(modelOptions.name, associationOptions.name, associationInstance[accessKey]));
23
+ if (associationOptions.innerAssociation &&
24
+ associationInstance[associationOptions.innerAssociation.alias]) {
25
+ depKeys.push(generateDependencyKey(modelOptions.name, associationOptions.innerAssociation.name, associationInstance[associationOptions.innerAssociation.alias][associationOptions.innerAssociation.accessKey]));
26
+ }
27
+ });
28
+ return depKeys;
29
+ }
30
+ return [
31
+ generateDependencyKey(modelOptions.name, associationOptions.name, instance[associationOptions.alias][accessKey]),
32
+ ];
33
+ });
34
+ return keys.reduce((flattenArray, array) => flattenArray.concat(array), []);
35
+ };
36
+ const handleTransactionHook = (instance, options, func) => {
37
+ const { transaction } = options;
38
+ if (transaction) {
39
+ transaction.afterCommit(() => func(instance));
40
+ }
41
+ else {
42
+ func(instance);
43
+ }
44
+ };
45
+ class SequelizeAdapter {
46
+ constructor(sequelize, debug) {
47
+ this.ormInstance = sequelize;
48
+ this.debugMode = debug;
49
+ }
50
+ getModel(modelName) {
51
+ return this.ormInstance.models[modelName];
52
+ }
53
+ getModelDependencies(modelName) {
54
+ const { associations } = this.ormInstance.models[modelName];
55
+ return [
56
+ ...Object.keys(associations).map(association => {
57
+ const sequelizeAssociation = associations[association];
58
+ const associationOptions = {
59
+ alias: sequelizeAssociation.as,
60
+ name: sequelizeAssociation.target.name,
61
+ accessKey: sequelizeAssociation.target.primaryKeyAttribute,
62
+ };
63
+ if (sequelizeAssociation.through) {
64
+ const relationModel = sequelizeAssociation.through.model;
65
+ associationOptions.innerAssociation = {
66
+ alias: relationModel.name,
67
+ name: relationModel.name,
68
+ accessKey: relationModel.primaryKeyAttribute,
69
+ };
70
+ }
71
+ return associationOptions;
72
+ }),
73
+ ];
74
+ }
75
+ debug(message, payload) {
76
+ if (this.debugMode) {
77
+ logger_1.default.info(`[ORM_CACHE Debug] ${message}`, payload);
78
+ }
79
+ }
80
+ injectGetWithCacheFunction(cache, modelOptions) {
81
+ const addDependencies = async (instance) => {
82
+ const dependencyKeys = getInstanceDependencyKeys(modelOptions, instance);
83
+ const instanceKey = generateInstanceKey(modelOptions.name, instance.id);
84
+ this.debug('Adding dependencies', { instanceKey, dependencyKeys });
85
+ const addDependenciesMulti = cache.getClient().multi();
86
+ const addDependenciesMultiAsync = (0, util_1.promisify)(addDependenciesMulti.exec).bind(addDependenciesMulti);
87
+ dependencyKeys.reduce((multi, key) => multi.sadd(key, instanceKey), addDependenciesMulti);
88
+ return addDependenciesMultiAsync();
89
+ };
90
+ const model = this.getModel(modelOptions.name);
91
+ model.findByPkCached = async (id, scopes, options) => {
92
+ const cacheKey = generateInstanceKey(modelOptions.name, id);
93
+ let value = JSON.parse(await cache.getClient().getAsync(cacheKey));
94
+ if (!value) {
95
+ this.debug('Value not found in cache, looking in db', { id, cacheKey });
96
+ value = await model.scope(scopes).findByPk(id, options);
97
+ this.debug('Value from DB', { value: value || 'not found', cacheKey });
98
+ await Promise.all([
99
+ cache.getClient().setAsync(cacheKey, JSON.stringify(value)),
100
+ value && addDependencies(value),
101
+ ]);
102
+ }
103
+ else {
104
+ value = this.getModel(modelOptions.name).build(value, { isNewRecord: false, include: options.include });
105
+ this.debug('Found cached value', { value, id, cacheKey });
106
+ }
107
+ return value;
108
+ };
109
+ }
110
+ addInvalidationHooks(cache, modelOptions) {
111
+ const invalidateModelInstance = async (instance) => {
112
+ const dependencyKeys = getInstanceDependencyKeys(modelOptions, instance);
113
+ const instanceKey = generateInstanceKey(modelOptions.name, instance.id);
114
+ this.debug('Removing dependencies', { instance, instanceKey, dependencyKeys });
115
+ const removeMulti = cache.getClient().multi();
116
+ const removeMultiAsync = (0, util_1.promisify)(removeMulti.exec).bind(removeMulti);
117
+ dependencyKeys.map(key => removeMulti.srem(key, instanceKey));
118
+ removeMulti.del(instanceKey);
119
+ return removeMultiAsync();
120
+ };
121
+ const invalidateModelInstanceByAssociation = async (association, associationId) => {
122
+ const dependentInstancesKeys = await cache.getClient().smembersAsync(generateDependencyKey(modelOptions.name, association, associationId));
123
+ this.debug('Invalidating dependent instances', { dependentInstancesKeys });
124
+ const removeMulti = cache.getClient().multi();
125
+ const removeMultiAsync = (0, util_1.promisify)(removeMulti.exec).bind(removeMulti);
126
+ const dependenciesToRemove = await Promise.all(dependentInstancesKeys.map(async (instanceKey) => {
127
+ const instance = JSON.parse(await cache.getClient().getAsync(instanceKey));
128
+ if (!instance) {
129
+ return [];
130
+ }
131
+ const dependencyKeys = getInstanceDependencyKeys(modelOptions, instance);
132
+ dependencyKeys.reduce((multi, key) => multi.srem(key, instanceKey), removeMulti);
133
+ removeMulti.del(instanceKey);
134
+ return dependencyKeys;
135
+ }));
136
+ this.debug('Removing dependencies', { dependentInstancesKeys, dependenciesToRemove });
137
+ return removeMultiAsync();
138
+ };
139
+ const model = this.getModel(modelOptions.name);
140
+ INVALIDATION_HOOKS.map(hook => model.addHook(hook, (instance, options) => handleTransactionHook(instance, options, instance => invalidateModelInstance(instance))));
141
+ BULK_HOOKS.map(hook => model.addHook(hook, options => options.individualHook = true));
142
+ modelOptions.associations = this.getModelDependencies(modelOptions.name);
143
+ this.debug(`Adding Invalidations Hooks to ${modelOptions.name}'s associations`, { associations: modelOptions.associations });
144
+ modelOptions.associations.map((associationOptions) => {
145
+ const associationModel = this.getModel(associationOptions.name);
146
+ INVALIDATION_HOOKS.map(hook => associationModel.addHook(hook, (instance, options) => handleTransactionHook(instance, options, associationInstance => invalidateModelInstanceByAssociation(associationOptions.name, associationInstance[associationOptions.accessKey]))));
147
+ BULK_HOOKS.map(hook => associationModel.addHook(hook, options => options.individualHook = true));
148
+ if (associationOptions.innerAssociation) {
149
+ const innerAssociationModel = this.getModel(associationOptions.innerAssociation.name);
150
+ INVALIDATION_HOOKS.map(hook => innerAssociationModel.addHook(hook, (instance, options) => handleTransactionHook(instance, options, innerAssociationInstance => invalidateModelInstanceByAssociation(associationOptions.innerAssociation.name, innerAssociationInstance[associationOptions.innerAssociation.accessKey]))));
151
+ BULK_HOOKS.map(hook => innerAssociationModel.addHook(hook, options => options.individualHook = true));
152
+ }
153
+ });
154
+ }
155
+ }
156
+ exports.default = SequelizeAdapter;
@@ -0,0 +1,13 @@
1
+ import { RedisError } from 'redis';
2
+ declare class RedisCacheError extends RedisError {
3
+ innerError: any;
4
+ message: string;
5
+ constructor(msg: string, err: any);
6
+ toString(): string;
7
+ }
8
+ declare class RedisLockError extends RedisCacheError {
9
+ key: string;
10
+ constructor(msg: string, err: any, key: string);
11
+ toString(): string;
12
+ }
13
+ export { RedisCacheError, RedisLockError, };
@@ -0,0 +1,25 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.RedisLockError = exports.RedisCacheError = void 0;
4
+ const redis_1 = require("redis");
5
+ class RedisCacheError extends redis_1.RedisError {
6
+ constructor(msg, err) {
7
+ super();
8
+ this.innerError = err;
9
+ this.message = msg;
10
+ }
11
+ toString() {
12
+ return `${this.message}: ${this.innerError}`;
13
+ }
14
+ }
15
+ exports.RedisCacheError = RedisCacheError;
16
+ class RedisLockError extends RedisCacheError {
17
+ constructor(msg, err, key) {
18
+ super(msg, err);
19
+ this.key = key;
20
+ }
21
+ toString() {
22
+ return `${this.message} (${this.key}): ${this.innerError}`;
23
+ }
24
+ }
25
+ exports.RedisLockError = RedisLockError;
@@ -0,0 +1,21 @@
1
+ declare class RedisCache {
2
+ private client;
3
+ private locker;
4
+ private lockTimeout;
5
+ private lockDuration;
6
+ locks: any;
7
+ private baseTTL;
8
+ private lockRetries;
9
+ keyPrefix: string;
10
+ useLock: boolean;
11
+ constructor(options: any);
12
+ get(key: any): Promise<any>;
13
+ getMultiple(keys: any): Promise<any>;
14
+ set(key: any, value: any): Promise<void>;
15
+ setMultiple(keyValues: Record<string, any>): Promise<any>;
16
+ remove(key: any): Promise<void>;
17
+ removeMultiple(keys: any): Promise<void>;
18
+ getClient(): any;
19
+ private lock;
20
+ }
21
+ export default RedisCache;
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const redis_1 = __importDefault(require("redis"));
7
+ const redis_lock_1 = __importDefault(require("redis-lock"));
8
+ const util_1 = require("util");
9
+ const errors_1 = require("./errors");
10
+ const logger_1 = __importDefault(require("../logger"));
11
+ const promisifyAll = (obj) => {
12
+ Object.keys(obj).forEach((key) => {
13
+ if (typeof obj[key] === 'function') {
14
+ obj[`${key}Async`] = (0, util_1.promisify)(obj[key]);
15
+ }
16
+ });
17
+ };
18
+ promisifyAll(redis_1.default.RedisClient.prototype);
19
+ promisifyAll(redis_1.default.Multi.prototype);
20
+ const { env } = process;
21
+ const HOST = env.REDIS_HOST_NAME || '127.0.0.1';
22
+ const PORT = env.REDIS_HOST_PORT || 6379;
23
+ const SERVICE_NAME = env.AF_SERVICE_NAME;
24
+ const DEFAULT_LOCK_TIMEOUT = 5000;
25
+ const DEFAULT_LOCK_DURATION = 1000;
26
+ const DEFAULT_BASE_TTL = 3600;
27
+ const KEY_PREFIX = SERVICE_NAME + ':';
28
+ if (!SERVICE_NAME) {
29
+ throw new Error('SERVICE_NAME cannot be null, please check your env.AF_SERVICE_NAME');
30
+ }
31
+ class RedisCache {
32
+ constructor(options) {
33
+ this.client = redis_1.default.createClient({
34
+ host: options.host || HOST,
35
+ port: options.port || PORT,
36
+ });
37
+ this.locker = (0, util_1.promisify)((0, redis_lock_1.default)(this.client, options.lockRetries ?? 10));
38
+ this.lockTimeout = options.lockTimeout ?? DEFAULT_LOCK_TIMEOUT;
39
+ this.lockDuration = options.lockDuration ?? DEFAULT_LOCK_DURATION;
40
+ this.baseTTL = options.ttl ?? DEFAULT_BASE_TTL;
41
+ this.locks = {};
42
+ this.useLock = !!options.useLock;
43
+ this.keyPrefix = KEY_PREFIX;
44
+ }
45
+ async get(key) {
46
+ const keyWithPrefix = KEY_PREFIX + key;
47
+ let value;
48
+ try {
49
+ // Try to get the value from redis.
50
+ value = await this.client.getAsync(keyWithPrefix);
51
+ }
52
+ catch (err) {
53
+ throw new errors_1.RedisCacheError('Failed to get a value', err);
54
+ }
55
+ if (this.useLock) {
56
+ let lock;
57
+ try {
58
+ // Try to lock the key.
59
+ lock = await this.lock(keyWithPrefix);
60
+ }
61
+ catch (err) {
62
+ throw new errors_1.RedisLockError('Failed to lock key', err, keyWithPrefix);
63
+ }
64
+ // If the lock did not fail, add it to a locks dictionary.
65
+ this.locks[keyWithPrefix] = lock;
66
+ }
67
+ return JSON.parse(value);
68
+ }
69
+ async getMultiple(keys) {
70
+ const keysWithPrefix = keys.map(key => KEY_PREFIX + key);
71
+ let values;
72
+ try {
73
+ if (keysWithPrefix.length === 0) {
74
+ return [];
75
+ }
76
+ // Try to get the value from redis.
77
+ values = await this.client.mgetAsync(keysWithPrefix);
78
+ }
79
+ catch (err) {
80
+ throw new errors_1.RedisCacheError('Failed to get a value', err);
81
+ }
82
+ return values.map(value => JSON.parse(value));
83
+ }
84
+ async set(key, value) {
85
+ const keyWithPrefix = KEY_PREFIX + key;
86
+ const ttl = parseInt(String(this.baseTTL * (Math.random() + 1)), 10);
87
+ try {
88
+ await this.client.setAsync(keyWithPrefix, JSON.stringify(value), 'EX', ttl);
89
+ if (this.locks[keyWithPrefix]) {
90
+ await this.locks[keyWithPrefix]();
91
+ delete this.locks[keyWithPrefix];
92
+ }
93
+ }
94
+ catch (err) {
95
+ throw new errors_1.RedisCacheError('Failed to set a key-value pair', err);
96
+ }
97
+ }
98
+ async setMultiple(keyValues) {
99
+ if (typeof keyValues !== 'object') {
100
+ const error = new errors_1.RedisCacheError('keyValues must be an object', new Error('keyValues must be an object'));
101
+ logger_1.default.error('keyValues must be an object', { error });
102
+ throw error;
103
+ }
104
+ const keyValuesWithPrefix = Object.entries(keyValues).map(([key, value]) => [KEY_PREFIX + key, JSON.stringify(value)]);
105
+ const ttl = parseInt(String(this.baseTTL * (Math.random() + 1)), 10);
106
+ try {
107
+ const multi = this.client.multi();
108
+ const setPromise = multi.msetAsync(...keyValuesWithPrefix.flat());
109
+ keyValuesWithPrefix.map(([key]) => {
110
+ return multi.expireAsync(key, ttl);
111
+ });
112
+ await multi.exec();
113
+ return setPromise;
114
+ }
115
+ catch (err) {
116
+ throw new errors_1.RedisCacheError('Failed to set multiple key-value pairs', err);
117
+ }
118
+ }
119
+ async remove(key) {
120
+ const keyWithPrefix = KEY_PREFIX + key;
121
+ try {
122
+ if (this.locks[keyWithPrefix]) {
123
+ await this.locks[keyWithPrefix]();
124
+ delete this.locks[keyWithPrefix];
125
+ }
126
+ await this.client.delAsync(keyWithPrefix);
127
+ }
128
+ catch (err) {
129
+ throw new errors_1.RedisCacheError(`Failed to delete key ${keyWithPrefix}`, err);
130
+ }
131
+ }
132
+ async removeMultiple(keys) {
133
+ const keysWithPrefix = keys.map(key => KEY_PREFIX + key);
134
+ try {
135
+ await this.client.delAsync(keysWithPrefix);
136
+ }
137
+ catch (err) {
138
+ throw new errors_1.RedisCacheError(`Failed to delete multiple keys ${keysWithPrefix.join('|')}`, err);
139
+ }
140
+ }
141
+ getClient() {
142
+ return this.client;
143
+ }
144
+ lock(key) {
145
+ return Promise.race([
146
+ this.locker(key, this.lockDuration),
147
+ new Promise((resolve, reject) => setTimeout(() => reject(), this.lockTimeout)),
148
+ ]);
149
+ }
150
+ }
151
+ exports.default = RedisCache;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const redis_1 = __importDefault(require("redis"));
7
+ const index_1 = __importDefault(require("./index"));
8
+ const util_1 = require("util");
9
+ const promisifyAll = (obj) => {
10
+ Object.keys(obj).forEach((key) => {
11
+ if (typeof obj[key] === 'function') {
12
+ obj[`${key}Async`] = (0, util_1.promisify)(obj[key]);
13
+ }
14
+ });
15
+ };
16
+ promisifyAll(redis_1.default.RedisClient.prototype);
17
+ describe('RedisCache', () => {
18
+ let redisCache;
19
+ const testClient = redis_1.default.createClient();
20
+ beforeEach(() => {
21
+ redisCache = new index_1.default({});
22
+ });
23
+ afterEach(() => {
24
+ jest.clearAllMocks();
25
+ });
26
+ describe('setMultiple', () => {
27
+ it('should set multiple key-value pairs in redis', async () => {
28
+ const keyValues = { testKey1: 'testValue1', testKey2: 'testValue2' };
29
+ const keyValuesArray = Object.entries(keyValues);
30
+ await redisCache.setMultiple(keyValues);
31
+ const expectedKeyValues = keyValuesArray.map(([key, value]) => [`${redisCache.keyPrefix}${key}`, JSON.stringify(value)]);
32
+ await Promise.all(expectedKeyValues.map(async ([key, value]) => {
33
+ const valueInRedis = await testClient.getAsync(key);
34
+ expect(valueInRedis).toBe(value);
35
+ }));
36
+ // Check that the keys expire after a random amount of time.
37
+ const currentTTL = await testClient.ttlAsync(expectedKeyValues[0][0]);
38
+ expect(currentTTL).toBeGreaterThanOrEqual(0);
39
+ });
40
+ });
41
+ describe('set', () => {
42
+ it('should set a key-value pair in redis', async () => {
43
+ const key = 'testKey';
44
+ const value = 'testValue';
45
+ await redisCache.set(key, value);
46
+ const keyWithPrefix = `${redisCache.keyPrefix}${key}`;
47
+ const valueInRedis = await testClient.getAsync(keyWithPrefix);
48
+ expect(valueInRedis).toBe(JSON.stringify(value));
49
+ // Check that the key expires after a random amount of time.
50
+ const currentTTL = await testClient.ttlAsync(keyWithPrefix);
51
+ expect(currentTTL).toBeGreaterThanOrEqual(0);
52
+ });
53
+ it('should release the lock after setting a key-value pair', async () => {
54
+ const key = 'testKeyWithLock';
55
+ const value = 'testValueWithLock';
56
+ redisCache.useLock = true;
57
+ await redisCache.set(key, value);
58
+ const keyWithPrefix = `${redisCache.keyPrefix}${key}`;
59
+ const valueInRedis = await testClient.getAsync(keyWithPrefix);
60
+ expect(valueInRedis).toBe(JSON.stringify(value));
61
+ // Check that the lock is released
62
+ expect(redisCache.locks[keyWithPrefix]).toBeUndefined();
63
+ });
64
+ });
65
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@autofleet/matmon",
3
- "version": "2.3.0",
3
+ "version": "2.3.1-beta-1",
4
4
  "description": "manage cache",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",