118 lines
3.2 KiB
TypeScript
118 lines
3.2 KiB
TypeScript
// Copyright 2018-2022 Signal Messenger, LLC
|
|
// SPDX-License-Identifier: AGPL-3.0-only
|
|
|
|
import { readFileSync, writeFileSync, unlinkSync } from 'fs';
|
|
|
|
import { get } from 'lodash';
|
|
import { set } from 'lodash/fp';
|
|
import { strictAssert } from '../ts/util/assert';
|
|
|
|
const ENCODING = 'utf8';
|
|
|
|
type InternalConfigType = Record<string, unknown>;
|
|
|
|
export type ConfigType = {
|
|
set: (keyPath: string, value: unknown) => void;
|
|
get: (keyPath: string) => unknown;
|
|
remove: () => void;
|
|
|
|
// Test-only
|
|
_getCachedValue: () => InternalConfigType | undefined;
|
|
};
|
|
|
|
export function start({
|
|
name,
|
|
targetPath,
|
|
throwOnFilesystemErrors,
|
|
}: Readonly<{
|
|
name: string;
|
|
targetPath: string;
|
|
throwOnFilesystemErrors: boolean;
|
|
}>): ConfigType {
|
|
let cachedValue: InternalConfigType = Object.create(null);
|
|
let incomingJson: string | undefined;
|
|
|
|
try {
|
|
incomingJson = readFileSync(targetPath, ENCODING);
|
|
cachedValue = incomingJson ? JSON.parse(incomingJson) : undefined;
|
|
console.log(`config/get: Successfully read ${name} config file`);
|
|
|
|
if (!cachedValue) {
|
|
console.log(
|
|
`config/start: ${name} config value was falsy, cache is now empty object`
|
|
);
|
|
cachedValue = Object.create(null);
|
|
}
|
|
} catch (error) {
|
|
if (throwOnFilesystemErrors && error.code !== 'ENOENT') {
|
|
throw error;
|
|
}
|
|
|
|
if (incomingJson) {
|
|
console.log(
|
|
`config/start: ${name} config file was malformed, starting afresh`
|
|
);
|
|
} else {
|
|
console.log(
|
|
`config/start: Did not find ${name} config file (or it was empty), cache is now empty object`
|
|
);
|
|
}
|
|
cachedValue = Object.create(null);
|
|
}
|
|
|
|
function ourGet(keyPath: string): unknown {
|
|
return get(cachedValue, keyPath);
|
|
}
|
|
|
|
function ourSet(keyPath: string, value: unknown): void {
|
|
const newCachedValue = set(keyPath, value, cachedValue);
|
|
|
|
console.log(`config/set: Saving ${name} config to disk`);
|
|
|
|
if (!throwOnFilesystemErrors) {
|
|
cachedValue = newCachedValue;
|
|
}
|
|
const outgoingJson = JSON.stringify(newCachedValue, null, ' ');
|
|
try {
|
|
writeFileSync(targetPath, outgoingJson, ENCODING);
|
|
console.log(`config/set: Saved ${name} config to disk`);
|
|
cachedValue = newCachedValue;
|
|
} catch (err: unknown) {
|
|
if (throwOnFilesystemErrors) {
|
|
throw err;
|
|
} else {
|
|
console.warn(
|
|
`config/set: Failed to save ${name} config to disk; only updating in-memory data`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function remove(): void {
|
|
console.log(`config/remove: Deleting ${name} config from disk`);
|
|
try {
|
|
unlinkSync(targetPath);
|
|
console.log(`config/remove: Deleted ${name} config from disk`);
|
|
} catch (err: unknown) {
|
|
const errCode: unknown = get(err, 'code');
|
|
if (throwOnFilesystemErrors) {
|
|
strictAssert(errCode === 'ENOENT', 'Expected deletion of no file');
|
|
console.log(`config/remove: No ${name} config on disk, did nothing`);
|
|
} else {
|
|
console.warn(
|
|
`config/remove: Got ${String(
|
|
errCode
|
|
)} when removing ${name} config from disk`
|
|
);
|
|
}
|
|
}
|
|
cachedValue = Object.create(null);
|
|
}
|
|
|
|
return {
|
|
set: ourSet,
|
|
get: ourGet,
|
|
remove,
|
|
_getCachedValue: () => cachedValue,
|
|
};
|
|
}
|