1 | const commonUtils = require('./common-utils');
|
2 | const deepcopy = function(obj) {
|
3 | return JSON.parse(JSON.stringify(obj));
|
4 | };
|
5 | const util = require('util');
|
6 |
|
7 | const pDebounce = require('p-debounce');
|
8 |
|
9 | const checkCacheSize = (cache, maxSize) => {
|
10 | return new Promise(resolve => {
|
11 |
|
12 | if(memorySizeOf(cache) > maxSize) {
|
13 | console.warn('The cache is too big! Help!');
|
14 |
|
15 | } else {
|
16 | resolve();
|
17 | }
|
18 | });
|
19 | };
|
20 |
|
21 | const debouncedCheckCacheSize = pDebounce(checkCacheSize, 10000);
|
22 |
|
23 |
|
24 | const createCacheRecord = function (body) {
|
25 | const timestamp = new Date();
|
26 | const cacheRecord = {
|
27 | timestamp: timestamp,
|
28 | lastUsed: timestamp,
|
29 | body: body,
|
30 | estimatedSize: null
|
31 | };
|
32 |
|
33 | return cacheRecord;
|
34 | };
|
35 |
|
36 | const getHrefType = (fullHref, isList) => {
|
37 | const containsQuestionMark = fullHref.match(/^.*\?.*$/g);
|
38 | if(containsQuestionMark) {
|
39 | if(fullHref.toLowerCase().match(/\?(limit\=[^&]+|offset\=[^&]+|keyoffset\=[^&]+|hrefs\=[^&]+|expand\=(full|summary|none))(&(limit\=[^&]+|offset\=[^&]+|keyoffset\=[^&]+|hrefs\=[^&]+|expand\=(full|summary|none)))*$/g)) {
|
40 | return 'BASIC_LIST';
|
41 | } else {
|
42 | return 'COMPLEX';
|
43 | }
|
44 | } else {
|
45 | if(isList) {
|
46 | return 'BASIC_LIST';
|
47 | } else {
|
48 | return 'PERMALINK';
|
49 | }
|
50 | }
|
51 | };
|
52 |
|
53 | function memorySizeOf(object) {
|
54 | var objectList = [];
|
55 | var stack = [ object ];
|
56 | var bytes = 0;
|
57 |
|
58 | while ( stack.length ) {
|
59 | var value = stack.pop();
|
60 |
|
61 | if ( typeof value === 'boolean' ) {
|
62 | bytes += 4;
|
63 | }
|
64 | else if ( typeof value === 'string' ) {
|
65 | bytes += value.length * 2;
|
66 | }
|
67 | else if ( typeof value === 'number' ) {
|
68 | bytes += 8;
|
69 | }
|
70 | else if (typeof value === 'object' && objectList.indexOf( value ) === -1) {
|
71 | objectList.push( value );
|
72 |
|
73 | for( var i in value ) {
|
74 | stack.push( value[ i ] );
|
75 | }
|
76 | }
|
77 | }
|
78 | return bytes;
|
79 | };
|
80 |
|
81 | module.exports = class Cache {
|
82 | constructor(config = {}, api) {
|
83 | this.timeout = config.timeout || 0;
|
84 | this.maxSize = config.maxSize || 10;
|
85 | this.api = api;
|
86 | if(config.initialise && !Array.isArray(config.initialise)) {
|
87 | this.initialConfig = [config.initialise];
|
88 | } else {
|
89 | this.initialConfig = config.initialise;
|
90 | }
|
91 | this.cache = {
|
92 | basicLists: {},
|
93 | complexHrefs: {},
|
94 | totalSize: 0
|
95 | };
|
96 | }
|
97 |
|
98 |
|
99 | initialise() {
|
100 | if(this.initialConfig) {
|
101 | this.initialConfig.forEach(init => {
|
102 | init.hrefs.forEach(href => {
|
103 | const cacheConfig = {timeout: init.timeout ? init.timeout : this.timeout};
|
104 | this.api.getAll(href, undefined, {caching: cacheConfig});
|
105 | });
|
106 | });
|
107 | }
|
108 | }
|
109 |
|
110 | async get(href, params, options = {}, isList) {
|
111 | const cacheOptions = options.caching || {};
|
112 | const timeout = cacheOptions.timeout || this.timeout;
|
113 | if(timeout === 0) {
|
114 | return this.api.getRaw(href, params, options);
|
115 | }
|
116 | const fullHref = commonUtils.parametersToString(href, params);
|
117 | const cacheRecord = this.getCacheRecord(fullHref, isList);
|
118 | if(!cacheRecord || (new Date().getTime() - cacheRecord.timestamp.getTime() > timeout * 1000)) {
|
119 | const logging = options.logging || this.api.configuration.logging;
|
120 | if(/caching/.test(logging)) {
|
121 | console.log('cache MISS for ' + fullHref);
|
122 | }
|
123 | const body = this.api.getRaw(href, params, options);
|
124 | this.updateCacheRecord(fullHref, isList, body);
|
125 | body.then(result => {
|
126 | if(isList && (!href.toLowerCase().match(/^.+[\?\&]expand\=.+$/) || href.toLowerCase().match(/^.+[\?\&]expand\=full.*$/))) {
|
127 | result.results.forEach(obj => {
|
128 | this.updateCacheRecord(obj.href, false, Promise.resolve(obj.$$expanded));
|
129 | });
|
130 | }
|
131 |
|
132 |
|
133 |
|
134 | });
|
135 | const resolvedBody = await body;
|
136 | return deepcopy(resolvedBody);
|
137 | } else {
|
138 | if(options.logging === 'cacheInfo' || options.logging === 'debug') {
|
139 | console.log('cache HIT for ' + fullHref);
|
140 | }
|
141 |
|
142 | cacheRecord.lastUsed = new Date;
|
143 | const resolvedBody = await cacheRecord.body;
|
144 | return deepcopy(resolvedBody);
|
145 | }
|
146 | }
|
147 |
|
148 | has(href, params, cacheOptions = {}, isList) {
|
149 | const timeout = cacheOptions.timeout || this.timeout;
|
150 | if(timeout === 0) {
|
151 | return false;
|
152 | }
|
153 | const fullHref = commonUtils.parametersToString(href, params);
|
154 | const cacheRecord = this.getCacheRecord(fullHref);
|
155 | return cacheRecord && (new Date().getTime() - cacheRecord.timestamp.getTime() <= timeout * 1000);
|
156 | }
|
157 |
|
158 | getCacheRecord(fullHref, isList) {
|
159 | switch(getHrefType(fullHref, isList)) {
|
160 | case 'PERMALINK':
|
161 | const parts = commonUtils.splitPermalink(fullHref);
|
162 | const group = this.cache[parts.path];
|
163 | return group ? group[parts.key] : undefined;
|
164 | break;
|
165 | case 'BASIC_LIST':
|
166 | return this.cache.basicLists[fullHref];
|
167 | break;
|
168 | case 'COMPLEX':
|
169 | return this.cache.complexHrefs[fullHref];
|
170 | break;
|
171 | }
|
172 | }
|
173 |
|
174 | updateCacheRecord(fullHref, isList, body) {
|
175 | const cacheRecord = createCacheRecord(body);
|
176 | switch(getHrefType(fullHref, isList)) {
|
177 | case 'PERMALINK':
|
178 | const parts = commonUtils.splitPermalink(fullHref);
|
179 | if(!this.cache[parts.path]) {
|
180 | this.cache[parts.path] = {};
|
181 | }
|
182 | this.cache[parts.path][parts.key] = cacheRecord;
|
183 | break;
|
184 | case 'BASIC_LIST':
|
185 | this.cache.basicLists[fullHref] = cacheRecord;
|
186 | break;
|
187 | case 'COMPLEX':
|
188 | return this.cache.complexHrefs[fullHref] = cacheRecord;
|
189 | break;
|
190 | }
|
191 | return cacheRecord;
|
192 | }
|
193 |
|
194 | onResourceUpdated(permalink) {
|
195 | this.cache.complex = {};
|
196 | const parts = commonUtils.splitPermalink(permalink);
|
197 | Object.keys(this.cache.basicLists).forEach(entry => {
|
198 | if(entry.startsWith(parts.path)) {
|
199 | delete this.cache.basicLists[entry];
|
200 | }
|
201 | });
|
202 | const group = this.cache[parts.path];
|
203 | if(group) {
|
204 | delete group[parts.key];
|
205 | }
|
206 | }
|
207 |
|
208 | onBatchPerformed(batch) {
|
209 | batch.forEach(outerBatchElem => {
|
210 | if(!Array.isArray(outerBatchElem)) {
|
211 | outerBatchElem = [outerBatchElem];
|
212 | }
|
213 | outerBatchElem.forEach(batchElem => {
|
214 | if(batchElem.verb === 'PUT' || batchElem.verb === 'DELETE') {
|
215 | this.onResourceUpdated(batchElem.href);
|
216 | }
|
217 | });
|
218 | });
|
219 | }
|
220 |
|
221 | onDataAltered(href, payload, method) {
|
222 | if((method === 'PUT' || method === 'POST') && href.match(/\/batch$/)) {
|
223 | this.onBatchPerformed(payload);
|
224 | } else if(method === 'PUT' || method === 'DELETE') {
|
225 | this.onResourceUpdated(href);
|
226 | }
|
227 | }
|
228 | };
|