Skip to content

Commit e70e846

Browse files
authored
Merge pull request #275 from appcelerator/APIGOV-32124
APIGOV-32124 Engage CLI - Migrate & Refactor Caching
2 parents d198eeb + 7707cbb commit e70e846

4 files changed

Lines changed: 257 additions & 0 deletions

File tree

package-lock.json

Lines changed: 51 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@
3232
"chalk": "^5.6.2",
3333
"ci-info": "^4.3.1",
3434
"cli-table3": "^0.6.5",
35+
"dayjs": "^1.11.19",
3536
"debug": "^4.4.3",
3637
"fastest-levenshtein": "^1.0.16",
3738
"got": "^14.6.2",
@@ -40,6 +41,7 @@
4041
"jose": "^6.1.0",
4142
"keytar": "7.9.0",
4243
"lodash": "^4.17.21",
44+
"node-cache": "^5.1.2",
4345
"pluralize": "^8.0.0",
4446
"pretty-bytes": "^7.1.0",
4547
"pretty-ms": "^9.3.0",
@@ -50,6 +52,7 @@
5052
"devDependencies": {
5153
"@koa/router": "^14.0.0",
5254
"@oclif/test": "^4.1.14",
55+
"@types/fs-extra": "^11.0.4",
5356
"@types/lodash": "^4.17.20",
5457
"@types/node": "^22",
5558
"@typescript-eslint/eslint-plugin": "^8.46.3",

src/lib/cache/CacheController.ts

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import dayjs from "dayjs";
2+
import {
3+
lstatSync,
4+
outputJsonSync,
5+
pathExistsSync,
6+
readFileSync,
7+
} from "fs-extra";
8+
import pkg from "lodash";
9+
import NodeCache from "node-cache";
10+
import { homedir } from "os";
11+
import path from "path";
12+
import { CACHE_FILE_TTL_MILLISECONDS, MAX_CACHE_FILE_SIZE } from "../types.js";
13+
import { isValidJson, writeToFile } from "../utils/utils.js";
14+
import logger from "../logger.js";
15+
16+
const { log } = logger("axway-cli: CacheController");
17+
const { isEmpty } = pkg;
18+
19+
interface Cache {
20+
set(key: string, value: object): CacheControllerClass;
21+
get(key: string): any;
22+
readFromFile(): CacheControllerClass;
23+
writeToFile(): CacheControllerClass;
24+
}
25+
26+
interface StoredCache {
27+
data?: object;
28+
metadata?: {
29+
modifyTimestamp?: number;
30+
schemaVersion?: string;
31+
};
32+
}
33+
34+
/**
35+
* Note: this file intentionally exporting only a single instance of CacheController,
36+
* since its possible to face a race condition when multiple instances will try to read/write file at the same time
37+
* Please do not use this class directly or rework the logic before.
38+
*/
39+
class CacheControllerClass implements Cache {
40+
public cacheFilePath = path.join(
41+
homedir(),
42+
".axway",
43+
"central",
44+
"cache.json",
45+
);
46+
private cache = new NodeCache();
47+
48+
constructor() {
49+
// note: init cache fire only once since using only a single instance of the class, remove if this will change
50+
this.initCacheFile();
51+
this.readFromFile();
52+
}
53+
54+
/**
55+
* Inits and validate cache file, should run once before using this class in the code (initialized in cli.ts currently)
56+
* An empty JSON file will be created if it is not exists of the file size is more than some value.
57+
*/
58+
initCacheFile() {
59+
try {
60+
if (pathExistsSync(this.cacheFilePath)) {
61+
log(`init, cache file found at ${this.cacheFilePath}`);
62+
const stats = lstatSync(this.cacheFilePath);
63+
log(`init, cache file size: ${Math.round(stats.size / 1000)} kb`);
64+
if (stats.size >= MAX_CACHE_FILE_SIZE) {
65+
// validating the size
66+
log(
67+
`init, cache size is exceeding the max allowed size of ${Math.round(
68+
MAX_CACHE_FILE_SIZE / 1000,
69+
)} kb, resetting the file`,
70+
);
71+
outputJsonSync(this.cacheFilePath, {});
72+
} else if (!isValidJson(readFileSync(this.cacheFilePath, "utf8"))) {
73+
// validating the content
74+
log("init, cache content is invalid, resetting the file ");
75+
outputJsonSync(this.cacheFilePath, {});
76+
}
77+
} else {
78+
log(
79+
`init, cache file not found, creating an empty one at ${this.cacheFilePath}`,
80+
);
81+
outputJsonSync(this.cacheFilePath, {});
82+
}
83+
} catch (e) {
84+
log(`cannot initialize cache file`, e);
85+
}
86+
}
87+
88+
/**
89+
* Set the key in memory cache.
90+
* @param key cache key to set
91+
* @param value value to set, note that setting "undefined" value will result in "null" value stored
92+
* @returns CacheController instance
93+
*/
94+
set(key: string, value: any): CacheControllerClass {
95+
this.cache.set(key, value);
96+
return this;
97+
}
98+
99+
/**
100+
* Returns the key value from the memory cache.
101+
* @param key key to get
102+
* @returns key value
103+
*/
104+
get(key: string): any | undefined {
105+
return this.cache.get(key);
106+
}
107+
108+
/**
109+
* Load stored cache from the file into memory and checks its timestamp.
110+
* If the timestamp is more than X days old it will reset the file without any changes to cache.
111+
* Note: using this method before writeToFile() will override keys in memory cache with the same name.
112+
* @returns CacheController instance
113+
*/
114+
readFromFile() {
115+
try {
116+
log("reading cache from the file");
117+
const jsonData = readFileSync(this.cacheFilePath, "utf8");
118+
const storedCache = JSON.parse(jsonData);
119+
120+
// validate values stored in the cache, reset the content of the file if its not empty already.
121+
if (
122+
storedCache.data &&
123+
storedCache.metadata &&
124+
storedCache.metadata.modifyTimestamp &&
125+
dayjs().diff(storedCache.metadata.modifyTimestamp, "milliseconds") <
126+
CACHE_FILE_TTL_MILLISECONDS
127+
) {
128+
for (const [key, val] of Object.entries(storedCache.data)) {
129+
if (storedCache.data.hasOwnProperty(key)) {
130+
this.cache.set(key, val);
131+
}
132+
}
133+
} else if (!isEmpty(storedCache)) {
134+
log(
135+
"timestamp or content is not valid and file is not empty, resetting the cache file",
136+
);
137+
outputJsonSync(this.cacheFilePath, {});
138+
}
139+
} catch (e) {
140+
log("cannot read cache from the file", e);
141+
} finally {
142+
return this;
143+
}
144+
}
145+
146+
/**
147+
* Writes current set of keys to the json file with following structure:
148+
* {
149+
* metadata: {
150+
* modifyTimestamp: current timestamp, used on read for TTL validation
151+
* schemaVersion: indicates the version of cache file structure, can be used later on if changing it.
152+
* },
153+
* data: {} key-value cache data
154+
* }
155+
* @returns CacheController instance
156+
*/
157+
writeToFile() {
158+
try {
159+
log("writing cache to the file");
160+
const keys = this.cache.keys();
161+
const cachedData = this.cache.mget(keys);
162+
const dataToStore: StoredCache = {
163+
metadata: {
164+
modifyTimestamp: Date.now(),
165+
schemaVersion: "1",
166+
},
167+
data: cachedData,
168+
};
169+
writeToFile(this.cacheFilePath, JSON.stringify(dataToStore));
170+
} catch (e) {
171+
log("cannot write cache to the file", e);
172+
} finally {
173+
return this;
174+
}
175+
}
176+
}
177+
178+
export const CacheController = new CacheControllerClass();

src/lib/utils/utils.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,28 @@
1+
import { writeFileSync } from "fs-extra";
2+
3+
export const writeToFile = (path: string, data: any): void => {
4+
try {
5+
writeFileSync(path, data);
6+
} catch (e) {
7+
// if parser is failing, rethrow with our own error
8+
throw new Error(`Error while writing the yaml file to: ${path}`);
9+
}
10+
};
11+
12+
/**
13+
* Checks if the passed item can be converted to a JSON or is a valid JSON object.
14+
* @param item item to check
15+
* @returns true if the item can be converted, false otherwise.
16+
*/
17+
export const isValidJson = (item: any) => {
18+
let parsedItem = typeof item !== "string" ? JSON.stringify(item) : item;
19+
try {
20+
parsedItem = JSON.parse(parsedItem);
21+
} catch (e) {
22+
return false;
23+
}
24+
return typeof parsedItem === "object" && item !== null;
25+
};
126
import chalk from "chalk";
227
import {
328
ApiServerVersions,

0 commit comments

Comments
 (0)