Skip to content

Commit e623241

Browse files
authored
feat: add TTLConfig (#538)
* feat: add TTLConfig * feat: add getLatestEntry and getLatestKey * chore: add tests * fix: handle no entries * chore: code review
1 parent ec68bb1 commit e623241

File tree

3 files changed

+164
-0
lines changed

3 files changed

+164
-0
lines changed

src/config/ttlConfig.ts

+63
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/*
2+
* Copyright (c) 2022, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { Duration } from '@salesforce/kit';
9+
import { JsonMap, Nullable } from '@salesforce/ts-types';
10+
import { ConfigFile } from './configFile';
11+
12+
/**
13+
* A Time To Live configuration file where each entry is timestamped and removed once the TTL has expired.
14+
*
15+
* @example
16+
* import { Duration } from '@salesforce/kit';
17+
* const config = await TTLConfig.create({
18+
* isGlobal: false,
19+
* ttl: Duration.days(1)
20+
* });
21+
*/
22+
export class TTLConfig<T extends TTLConfig.Options, P extends JsonMap> extends ConfigFile<T, TTLConfig.Contents<P>> {
23+
public set(key: string, value: Partial<TTLConfig.Entry<P>>): void {
24+
super.set(key, this.timestamp(value));
25+
}
26+
27+
public getLatestEntry(): Nullable<[string, TTLConfig.Entry<P>]> {
28+
const entries = this.entries() as Array<[string, TTLConfig.Entry<P>]>;
29+
const sorted = entries.sort(([, valueA], [, valueB]) => {
30+
return new Date(valueB.timestamp).getTime() - new Date(valueA.timestamp).getTime();
31+
});
32+
return sorted.length > 0 ? sorted[0] : null;
33+
}
34+
35+
public getLatestKey(): Nullable<string> {
36+
const [key] = this.getLatestEntry() || [null];
37+
return key;
38+
}
39+
40+
public isExpired(dateTime: number, value: P & { timestamp: string }): boolean {
41+
return dateTime - new Date(value.timestamp).getTime() > this.options.ttl.milliseconds;
42+
}
43+
44+
protected async init(): Promise<void> {
45+
const contents = await this.read(this.options.throwOnNotFound);
46+
const purged = {} as TTLConfig.Contents<P>;
47+
const date = new Date().getTime();
48+
for (const [key, opts] of Object.entries(contents)) {
49+
if (!this.isExpired(date, opts)) purged[key] = opts;
50+
}
51+
this.setContents(purged);
52+
}
53+
54+
private timestamp(value: Partial<TTLConfig.Entry<P>>): TTLConfig.Entry<P> {
55+
return { ...value, timestamp: new Date().toISOString() } as TTLConfig.Entry<P>;
56+
}
57+
}
58+
59+
export namespace TTLConfig {
60+
export type Options = ConfigFile.Options & { ttl: Duration };
61+
export type Entry<T extends JsonMap> = T & { timestamp: string };
62+
export type Contents<T extends JsonMap> = Record<string, Entry<T>>;
63+
}

src/exported.ts

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ export { OAuth2Config } from 'jsforce';
1212

1313
export { ConfigFile } from './config/configFile';
1414

15+
export { TTLConfig } from './config/ttlConfig';
16+
1517
export { envVars, EnvironmentVariable, SUPPORTED_ENV_VARS, EnvVars } from './config/envVars';
1618

1719
export { BaseConfigStore, ConfigContents, ConfigEntry, ConfigStore, ConfigValue } from './config/configStore';

test/unit/config/ttlConfigTest.ts

+99
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
/*
2+
* Copyright (c) 2022, salesforce.com, inc.
3+
* All rights reserved.
4+
* Licensed under the BSD 3-Clause license.
5+
* For full license text, see LICENSE.txt file in the repo root or https://opensource.org/licenses/BSD-3-Clause
6+
*/
7+
8+
import { expect } from 'chai';
9+
import { JsonMap } from '@salesforce/ts-types';
10+
import { Duration, sleep } from '@salesforce/kit';
11+
import { TTLConfig } from '../../../src/config/ttlConfig';
12+
import { testSetup } from '../../../src/testSetup';
13+
import { Global } from '../../../src/global';
14+
15+
const $$ = testSetup();
16+
17+
class TestConfig extends TTLConfig<TTLConfig.Options, JsonMap> {
18+
private static testId: string = $$.uniqid();
19+
20+
public static getTestLocalPath() {
21+
return $$.localPathRetrieverSync(TestConfig.testId);
22+
}
23+
24+
public static getDefaultOptions(isGlobal = false, filename?: string): TTLConfig.Options {
25+
return {
26+
rootFolder: $$.rootPathRetrieverSync(isGlobal, TestConfig.testId),
27+
isGlobal,
28+
isState: true,
29+
filename: filename || TestConfig.getFileName(),
30+
stateFolder: Global.SF_STATE_FOLDER,
31+
ttl: Duration.days(1),
32+
};
33+
}
34+
35+
public static getFileName() {
36+
return 'testFileName';
37+
}
38+
}
39+
40+
describe('TTLConfig', () => {
41+
describe('set', () => {
42+
it('should timestamp every entry', async () => {
43+
const config = await TestConfig.create();
44+
config.set('123', { foo: 'bar' });
45+
const entry = config.get('123');
46+
expect(entry).to.have.property('timestamp');
47+
});
48+
});
49+
50+
describe('getLatestEntry', () => {
51+
it('should return the latest entry', async () => {
52+
const config = await TestConfig.create();
53+
config.set('1', { one: 'one' });
54+
await sleep(1000);
55+
config.set('2', { two: 'two' });
56+
const latest = config.getLatestEntry();
57+
expect(latest).to.deep.equal(['2', config.get('2')]);
58+
});
59+
60+
it('should return null if there are no entries', async () => {
61+
const config = await TestConfig.create();
62+
const latest = config.getLatestEntry();
63+
expect(latest).to.equal(null);
64+
});
65+
});
66+
67+
describe('getLatestKey', () => {
68+
it('should return the key of the latest entry', async () => {
69+
const config = await TestConfig.create();
70+
config.set('1', { one: 'one' });
71+
await sleep(1000);
72+
config.set('2', { two: 'two' });
73+
const latest = config.getLatestKey();
74+
expect(latest).to.equal('2');
75+
});
76+
77+
it('should return null if there are no entries', async () => {
78+
const config = await TestConfig.create();
79+
const latest = config.getLatestKey();
80+
expect(latest).to.equal(null);
81+
});
82+
});
83+
84+
describe('isExpired', () => {
85+
it('should return true if timestamp is older than TTL', async () => {
86+
const config = await TestConfig.create();
87+
config.set('1', { one: 'one' });
88+
const isExpired = config.isExpired(new Date().getTime() + Duration.days(7).milliseconds, config.get('1'));
89+
expect(isExpired).to.be.true;
90+
});
91+
92+
it('should return false if timestamp is not older than TTL', async () => {
93+
const config = await TestConfig.create();
94+
config.set('1', { one: 'one' });
95+
const isExpired = config.isExpired(new Date().getTime(), config.get('1'));
96+
expect(isExpired).to.be.false;
97+
});
98+
});
99+
});

0 commit comments

Comments
 (0)