Skip to content

Commit e8ce13b

Browse files
feat: introduce jsonFileSettingsService to work with JSON files
Intorduce a new service - `jsonFileSettingsService` to work with JSON files in which you need to persist specific settings. The read/write operation of the file are locked, which makes the service usable in multiple cases. Having a separate service allows us to use multiple files to store data instead of writing everything in the user-settings.json file. To create the service reuse the logic from the old UserSettingsServiceBase and make it available through `$injector`. Add logic in the service to allow writing/reading data within specific cache timeout. In case you want to save data with cache information, just pass this setting to saveSetting method and the service will automatically add required information in the file. When you read information from the file, the service will check if you have passed cacheTimeout and will respect it when checking the value. In case you've passed cacheTimeout, but the value in the file does not have cache information, the service will return null as it is unable to determine if the cache is valid. In case there's cache information, the service will return the value in case the cache had not expired and null otherwise. In case the value in the file has cache information, but the readSetting is called without cacheTimeout, the value is returned immediately without checking the cache. The service is marked as non-shared, so for each `$injector.resolve("jsonFileSettingsService", { jsonFileSettingsPath: "myPath.json" })`, CLI will create a new instance of the service. As all actions are locked via lockfile, this shouldn't be a problem.
1 parent 478e56b commit e8ce13b

9 files changed

+548
-128
lines changed

lib/bootstrap.ts

+1
Original file line numberDiff line numberDiff line change
@@ -230,3 +230,4 @@ $injector.requirePublicClass("initializeService", "./services/initialize-service
230230

231231
$injector.require("npmConfigService", "./services/npm-config-service");
232232
$injector.require("ipService", "./services/ip-service");
233+
$injector.require("jsonFileSettingsService", "./common/services/json-file-settings-service");

lib/common/declarations.d.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -1229,9 +1229,8 @@ interface IPlistParser {
12291229
parseFileSync(plistFilePath: string): any;
12301230
}
12311231

1232-
interface IUserSettingsService extends UserSettings.IUserSettingsService {
1233-
loadUserSettingsFile(): Promise<void>;
1234-
saveSettings(data: IDictionary<{}>): Promise<void>;
1232+
interface IUserSettingsService extends IJsonFileSettingsService {
1233+
// keep for backwards compatibility
12351234
}
12361235

12371236
/**
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
interface ICacheTimeoutOpts {
2+
cacheTimeout: number;
3+
}
4+
5+
interface IUseCacheOpts {
6+
useCaching: boolean;
7+
}
8+
9+
interface IJsonFileSettingsService {
10+
getSettingValue<T>(settingName: string, cacheOpts?: ICacheTimeoutOpts): Promise<T>;
11+
saveSetting<T>(key: string, value: T, cacheOpts?: IUseCacheOpts): Promise<void>;
12+
removeSetting(key: string): Promise<void>;
13+
loadUserSettingsFile(): Promise<void>;
14+
saveSettings(data: IDictionary<{}>, cacheOpts?: IUseCacheOpts): Promise<void>;
15+
}

lib/common/definitions/user-settings.d.ts

-7
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
import * as path from "path";
2+
import { parseJson } from "../helpers";
3+
4+
export class JsonFileSettingsService implements IJsonFileSettingsService {
5+
private jsonSettingsFilePath: string = null;
6+
protected jsonSettingsData: any = null;
7+
private get lockFilePath(): string {
8+
return `${this.jsonSettingsFilePath}.lock`;
9+
}
10+
11+
constructor(jsonFileSettingsPath: string,
12+
private $fs: IFileSystem,
13+
private $lockService: ILockService,
14+
private $logger: ILogger) {
15+
this.jsonSettingsFilePath = jsonFileSettingsPath;
16+
}
17+
18+
public async getSettingValue<T>(settingName: string, cacheOpts?: { cacheTimeout: number }): Promise<T> {
19+
const action = async (): Promise<T> => {
20+
await this.loadUserSettingsFile();
21+
22+
if (this.jsonSettingsData && _.has(this.jsonSettingsData, settingName)) {
23+
const data = this.jsonSettingsData[settingName];
24+
const dataToReturn = data.modifiedByCacheMechanism ? data.value : data;
25+
if (cacheOpts && cacheOpts.cacheTimeout) {
26+
if (!data.modifiedByCacheMechanism) {
27+
// If data has no cache, but we want to check the timeout, consider the data as outdated.
28+
// this should be a really rare case
29+
return null;
30+
}
31+
32+
const currentTime = Date.now();
33+
if ((currentTime - data.time) > cacheOpts.cacheTimeout) {
34+
return null;
35+
}
36+
}
37+
38+
return dataToReturn;
39+
}
40+
41+
return null;
42+
};
43+
44+
return this.$lockService.executeActionWithLock<T>(action, this.lockFilePath);
45+
}
46+
47+
public async saveSetting<T>(key: string, value: T, cacheOpts?: { useCaching: boolean }): Promise<void> {
48+
const settingObject: any = {};
49+
settingObject[key] = value;
50+
51+
return this.saveSettings(settingObject, cacheOpts);
52+
}
53+
54+
public async removeSetting(key: string): Promise<void> {
55+
const action = async (): Promise<void> => {
56+
await this.loadUserSettingsFile();
57+
58+
delete this.jsonSettingsData[key];
59+
await this.saveSettings();
60+
};
61+
62+
return this.$lockService.executeActionWithLock<void>(action, this.lockFilePath);
63+
}
64+
65+
public saveSettings(data?: any, cacheOpts?: { useCaching: boolean }): Promise<void> {
66+
const action = async (): Promise<void> => {
67+
await this.loadUserSettingsFile();
68+
this.jsonSettingsData = this.jsonSettingsData || {};
69+
70+
_(data)
71+
.keys()
72+
.each(propertyName => {
73+
this.jsonSettingsData[propertyName] = cacheOpts && cacheOpts.useCaching && !data[propertyName].modifiedByCacheMechanism ? {
74+
time: Date.now(),
75+
value: data[propertyName],
76+
modifiedByCacheMechanism: true
77+
} : data[propertyName];
78+
});
79+
80+
this.$fs.writeJson(this.jsonSettingsFilePath, this.jsonSettingsData);
81+
};
82+
83+
return this.$lockService.executeActionWithLock<void>(action, this.lockFilePath);
84+
}
85+
86+
public async loadUserSettingsFile(): Promise<void> {
87+
if (!this.jsonSettingsData) {
88+
await this.loadUserSettingsData();
89+
}
90+
}
91+
92+
private async loadUserSettingsData(): Promise<void> {
93+
if (!this.$fs.exists(this.jsonSettingsFilePath)) {
94+
const unexistingDirs = this.getUnexistingDirectories(this.jsonSettingsFilePath);
95+
96+
this.$fs.writeFile(this.jsonSettingsFilePath, null);
97+
98+
// when running under 'sudo' we create the <path to home dir>/.local/share/.nativescript-cli dir with root as owner
99+
// and other Applications cannot access this directory anymore. (bower/heroku/etc)
100+
if (process.env.SUDO_USER) {
101+
for (const dir of unexistingDirs) {
102+
await this.$fs.setCurrentUserAsOwner(dir, process.env.SUDO_USER);
103+
}
104+
}
105+
}
106+
107+
const data = this.$fs.readText(this.jsonSettingsFilePath);
108+
109+
try {
110+
this.jsonSettingsData = parseJson(data);
111+
} catch (err) {
112+
this.$logger.trace(`Error while trying to parseJson ${data} data from ${this.jsonSettingsFilePath} file. Err is: ${err}`);
113+
this.$fs.deleteFile(this.jsonSettingsFilePath);
114+
}
115+
}
116+
117+
private getUnexistingDirectories(filePath: string): Array<string> {
118+
const unexistingDirs: Array<string> = [];
119+
let currentDir = path.join(filePath, "..");
120+
while (true) {
121+
// this directory won't be created.
122+
if (this.$fs.exists(currentDir)) {
123+
break;
124+
}
125+
unexistingDirs.push(currentDir);
126+
currentDir = path.join(currentDir, "..");
127+
}
128+
return unexistingDirs;
129+
}
130+
}
131+
132+
$injector.register("jsonFileSettingsService", JsonFileSettingsService, false);

lib/common/services/user-settings-service.ts

-107
This file was deleted.

0 commit comments

Comments
 (0)