Skip to content

Commit 11f3118

Browse files
committed
Added configurable cache-entry expiration.
1 parent d2cdfcd commit 11f3118

18 files changed

+450
-164
lines changed

README.md

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.2
1+
# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.3
22
33
![An stylised image of the project's logo formed of a lower-case i cursively joining a capitalised D](res/img/id-gum-logo.png)
44

@@ -27,10 +27,18 @@ A working version of node is required (Tested on `v21.6.2`).
2727
- Test reports are located [here](build/tests/html/vitest.html).
2828
- Coverage reports are located [here](build/tests/coverage/index.html).
2929

30+
`npm run check`
31+
- Runs: linting, auditing, testing.
32+
3033
### Linting
3134

3235
`npm run lint`
3336

37+
### Package Auditing
38+
39+
`npm run audit`
40+
41+
3442
## Production Plugins
3543

3644
- `discord.js`

VERSIONS.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@
33
0.0.1 - `First working private version that supports attached images.`
44

55
0.0.2 - `Functionality added to require the identification of emoji and custom (Discord) emoji.`
6+
7+
0.0.3 - `Cache expiration management.`

package-lock.json

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

package.json

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,16 @@
11
{
22
"name": "@ceilingcat/id-bot",
3-
"version": "0.0.2",
3+
"version": "0.0.3",
44
"description": "A discord bot to remind users to tag uploaded images with identifiers.",
55
"scripts": {
6+
"lint": "eslint \"src/*/*.js\"",
7+
"audit": "npm audit --registry=https://registry.npmjs.org",
8+
"test": "vitest --coverage --config ./vitest.config.ts",
9+
"check": "run-s lint audit test",
610
"start": "run-s execute",
711
"execute": "run-p auth-server id-bot",
8-
"test": "vitest --coverage --config ./vitest.config.ts",
912
"id-bot": "node src/js/node-starter.js",
10-
"auth-server": "node src/js/auth-server.js",
11-
"lint": "eslint \"src/*/*.js\""
13+
"auth-server": "node src/js/auth-server.js"
1214
},
1315
"main": "build/src/js/id-bot.js",
1416
"type": "module",

src/__tests__/application.spec.js

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,20 @@ const createIdBotReply = function (userMessage, replyContent) {
6969
};
7070

7171
describe("Application startup and shutdown", () => {
72-
let factory, process, client, discordInterfaceHarness, idBot, application;
72+
let factory, process, client, discordInterfaceHarness, cache, unboundCacheExpirator, idBot, application;
73+
74+
const withExpirator = (altCacheExpiratorProvider = undefined) => {
75+
cache = factory.createCache();
76+
unboundCacheExpirator = (altCacheExpiratorProvider && altCacheExpiratorProvider(cache)) ||
77+
factory.createCacheExpirator(cache);
78+
idBot = new IdBot(
79+
cache,
80+
unboundCacheExpirator,
81+
discordInterfaceHarness,
82+
new Logger(console, `IdBot(${CLIENT_ID})`)
83+
);
84+
application = new Application(process, idBot);
85+
};
7386

7487
beforeEach(() => {
7588
factory = new Factory();
@@ -88,32 +101,18 @@ describe("Application startup and shutdown", () => {
88101
factory,
89102
factory.createLogger(`DiscordInterface(${CLIENT_ID})`),
90103
CLIENT_ID);
91-
idBot = new IdBot(factory.createCache(), discordInterfaceHarness, new Logger(console, `IdBot(${CLIENT_ID})`));
92-
application = new Application(process, idBot);
104+
105+
withExpirator();
93106
});
94107

95-
// it("That failures to login are resolved by logging them and rethrowing.", () => {
96-
// factory = new Factory();
97-
// process = {on: vi.fn(), exit: vi.fn()};
98-
// client = {once: vi.fn(), on: vi.fn(), login: vi.fn().mockImplementationOnce(REJECTS_TO)};
99-
// discordInterfaceHarness = new DiscordInterfaceHarness(
100-
// client,
101-
// factory,
102-
// factory.createLogger(`DiscordInterface for ${CLIENT_ID}`),
103-
// CLIENT_ID);
104-
// idBot = new IdBot(factory.createCache(), discordInterfaceHarness, new Logger(console, "IdBot"));
105-
// let unhandledErrorCaught = false;
106-
// try {
107-
// new Application(process, idBot).start("");
108-
// } catch(e) {
109-
// unhandledErrorCaught = true;
110-
// }
111-
// expect(unhandledErrorCaught).toBe(true);
112-
// });
108+
afterEach(() => {
109+
application.stop();
110+
vi.restoreAllMocks();
111+
});
113112

114113
it("That messages authored by other bots are ignored even if they contain attachments that could be prompted in respect of.", () => {
115114
application.start(TOKEN);
116-
discordInterfaceHarness.onClientReady();
115+
discordInterfaceHarness.onClientReady();
117116

118117
const discordJsMessage = createDiscordJsMessage(CONTENT_EMPTY, ["image/png"], AUTHOR_ID__NOT_US, MessageType.Default, undefined, true);
119118
discordInterfaceHarness.onMessageCreate(discordJsMessage);
@@ -208,6 +207,43 @@ describe("Application startup and shutdown", () => {
208207
)
209208
);
210209

210+
describe(
211+
"That quickly culling cache entries means ",
212+
() => {
213+
it(
214+
`Something`,
215+
async () => {
216+
vi.useRealTimers();
217+
218+
withExpirator(c => factory.createCacheExpirator(c, 10, 1));
219+
220+
application.start(TOKEN);
221+
discordInterfaceHarness.onClientReady();
222+
223+
const userMessage = createDiscordJsMessage("ID: Something", []);
224+
discordInterfaceHarness.onMessageCreate(userMessage);
225+
226+
// Discord.js creation event for our previous reply, idBot caches referenced message to idBot reply if the message is not correctly identified
227+
discordInterfaceHarness.onMessageCreate(createIdBotReply(userMessage, OVER_DESCRIBED_REMINDER_MESSAGE));
228+
const updatedUserMessage = createDiscordJsMessage(
229+
"Blah blah blah.",
230+
[],
231+
AUTHOR_ID__NOT_US,
232+
MessageType.Default,
233+
undefined,
234+
false,
235+
userMessage.id
236+
);
237+
await new Promise(resolve => setTimeout(resolve, 2000));
238+
discordInterfaceHarness.onMessageUpdate(userMessage, updatedUserMessage);
239+
expect(updatedUserMessage.channel.messages.delete).not.toHaveBeenCalled();
240+
241+
application.stop();
242+
}
243+
);
244+
}
245+
);
246+
211247
it("That normal shutdown closes the IdBot instance and returns the correct process exit code", () => {
212248
application.start(TOKEN);
213249
application.stop();

src/__tests__/cache.spec.js

Lines changed: 72 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {beforeEach, describe, expect, it} from "vitest";
1+
import {beforeEach, describe, expect, it, vi} from "vitest";
22
import {Factory} from "../js/factory.js";
33
import {createUuid} from "./uuid.js";
44

@@ -17,83 +17,122 @@ describe("Tests for our Cache", () => {
1717
/**
1818
* @type *
1919
*/
20-
let key, value;
20+
let k0, v0, k1, v1;
21+
22+
const setK0V0 = () => {
23+
cache.set(k0, v0);
24+
25+
return cache;
26+
};
27+
28+
const setK1V1 = () => {
29+
cache.set(k1, v1);
30+
31+
return cache;
32+
};
2133

2234
beforeEach(() => {
2335
factory = new Factory();
2436
cache = factory.createCache();
25-
key = createUuid();
26-
value = createUuid();
37+
[k0, v0, k1, v1] = [createUuid(), createUuid(), createUuid(), createUuid()];
2738
});
2839

2940
it("We can store and retrieve things from the cache", () => {
30-
cache.set(key, value);
31-
32-
expect(cache.get(key)).toBe(value);
41+
expect(setK0V0().get(k0)).toBe(v0);
3342
});
3443

3544
it("Newly added cache entries have the required properties set", () => {
36-
cache.set(key, value);
37-
const meta = cache.getMeta(key);
45+
setK0V0();
3846

39-
expect(meta.key).toBe(key);
40-
expect(meta.value).toBe(value);
47+
const meta = cache.getMeta(k0);
48+
49+
expect(meta.key).toBe(k0);
4150
expect(meta.createdAt).toBeDefined();
4251
});
4352

4453
it("Newly added cache entries have a 'createdAt' value greater or equal than that sampled before population.", () => {
4554
const then = factory.utcTimeStampNow;
46-
cache.set(key, value);
47-
const meta = cache.getMeta(key);
55+
56+
const meta = setK0V0().getMeta(k0);
4857

4958
expect(meta.createdAt).toBeGreaterThanOrEqual(then);
5059
});
5160

5261
it("Metadata records the last accessed time of an entry correctly.", () => {
53-
cache.set(key, value);
54-
const metaBefore = cache.getMeta(key);
62+
setK0V0();
63+
const metaBefore = cache.getMeta(k0);
5564

56-
expect(metaBefore.lastAccessedAt).toBeUndefined();
65+
expect(metaBefore.lastAccessedAt).toEqual(metaBefore.createdAt);
5766

5867
const then = factory.utcTimeStampNow;
59-
cache.get(key);
60-
const metaAfter = cache.getMeta(key);
68+
cache.get(k0);
69+
const metaAfter = cache.getMeta(k0);
6170

6271
expect(metaAfter.lastAccessedAt).toBeGreaterThanOrEqual(then);
6372
});
6473

6574
it("Metadata records the last update time of an entry correctly.", () => {
66-
cache.set(key, value);
67-
const newValue = createUuid();
68-
const metaBefore = cache.getMeta(key);
75+
setK0V0();
76+
const newValue = v1;
77+
const metaBefore = cache.getMeta(k0);
6978

70-
expect(metaBefore.updatedAt).toBeUndefined();
71-
expect(metaBefore.value).toBe(value);
79+
expect(metaBefore.lastUpdatedAt).toBeUndefined();
7280

7381
const then = factory.utcTimeStampNow;
74-
cache.set(key, newValue);
75-
const metaAfter = cache.getMeta(key);
82+
cache.set(k0, newValue);
83+
const metaAfter = cache.getMeta(k0);
7684

77-
expect(metaAfter.updatedAt).toBeGreaterThanOrEqual(then);
78-
expect(metaAfter.value).toBe(newValue);
85+
expect(metaAfter.lastUpdatedAt).toBeGreaterThanOrEqual(then);
7986
});
8087

8188
it("Retrieving metadata returns undefined if the key is unknown.", () => {
82-
expect(cache.getMeta(key)).toBeUndefined();
89+
expect(cache.getMeta(k0)).toBeUndefined();
8390
});
8491

8592
it("Retrieving values returns undefined if the key is unknown.", () => {
86-
expect(cache.get(key)).toBeUndefined();
93+
expect(cache.get(k0)).toBeUndefined();
8794
});
8895

8996
it("Requests to remove unknown keys are allowed.", () => {
90-
expect(cache.remove(key)).toEqual(false);
97+
expect(cache.remove(k0)).toEqual(false);
9198
});
9299

93100
it("The removal by key works.", () => {
94-
cache.set(key, value);
95-
expect(cache.remove(key)).toBe(true);
96-
expect(cache.remove(key)).toBe(false);
101+
setK0V0();
102+
103+
expect(cache.remove(k0)).toBe(true);
104+
expect(cache.remove(k0)).toBe(false);
105+
});
106+
107+
it("That metaData returns the metadata elements.", () => {
108+
setK0V0();
109+
const metas = cache.metaData;
110+
/**
111+
* @type {CacheMeta}
112+
*/
113+
const firstElement = metas.next().value;
114+
expect(cache.get(firstElement.key)).toBe(v0);
115+
expect(metas.next().done).toBe(true);
116+
});
117+
118+
it("That metaData returns elements sorted by decreasing lastAccessedAt values.", async () => {
119+
vi.useRealTimers();
120+
121+
setK1V1();
122+
123+
await new Promise(resolve => setTimeout(resolve, 100));
124+
setK0V0();
125+
const metas = cache.metaData;
126+
127+
/**
128+
* @type {CacheMeta}
129+
*/
130+
const meta0 = metas.next().value;
131+
metas.next();
132+
133+
expect(metas.next().done).toBe(true);
134+
135+
expect(cache.get(meta0.key)).toBe(v1);
97136
});
98137

99138
});

src/__tests__/discord-interface-harness.js

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,17 @@
1-
import {assert} from "vitest";
21
import {DiscordInterface} from "../js/discord-interface";
32

43
class DiscordInterfaceHarness extends DiscordInterface {
54

6-
/**
7-
* @type {String}
8-
*/
9-
#channelId;
10-
115
/**
126
* @type {Map}
137
*/
148
#messageIdToMessage;
159

16-
constructor(client, factory, logger, clientId, channelId) {
10+
constructor(client, factory, logger, clientId) {
1711
super(client, factory, logger, clientId);
18-
this.#channelId = channelId;
1912
this.#messageIdToMessage = new Map();
2013
}
2114

22-
fetchMessage(channel, messageId, f) {
23-
assert(this.#channelId === channel.id);
24-
const message = this.#messageIdToMessage.get(messageId);
25-
f(this._createIdBotMessage(message));
26-
}
27-
2815
onClientReady() {
2916
super._onClientReady(this._client);
3017
}

0 commit comments

Comments
 (0)