Skip to content

Commit

Permalink
Cache expiration bug fixes and improved factory usage.
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisdenman committed Jun 30, 2024
1 parent 8c0f3d9 commit db1601d
Show file tree
Hide file tree
Showing 12 changed files with 88 additions and 85 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.3
# [ID Bot](https://github.com/chrisdenman/id-bot) 0.0.4
![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)

Expand Down
2 changes: 2 additions & 0 deletions VERSIONS.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,5 @@
0.0.2 - `Functionality added to require the identification of emoji and custom (Discord) emoji.`

0.0.3 - `Cache expiration management.`

0.0.4 - `Cache expiration bug fixes and improved factory usage.`
11 changes: 5 additions & 6 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@ceilingcat/id-bot",
"version": "0.0.3",
"version": "0.0.4",
"description": "A discord bot to remind users to tag uploaded images with identifiers.",
"scripts": {
"lint": "eslint \"src/*/*.js\"",
Expand Down
6 changes: 3 additions & 3 deletions src/__tests__/cache.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ describe("Tests for our Cache", () => {
});

it("Newly added cache entries have a 'createdAt' value greater or equal than that sampled before population.", () => {
const then = factory.utcTimeStampNow;
const then = factory.getNowInMilliSeconds;

const meta = setK0V0().getMeta(k0);

Expand All @@ -64,7 +64,7 @@ describe("Tests for our Cache", () => {

expect(metaBefore.lastAccessedAt).toEqual(metaBefore.createdAt);

const then = factory.utcTimeStampNow;
const then = factory.getNowInMilliSeconds;
cache.get(k0);
const metaAfter = cache.getMeta(k0);

Expand All @@ -78,7 +78,7 @@ describe("Tests for our Cache", () => {

expect(metaBefore.lastUpdatedAt).toBeUndefined();

const then = factory.utcTimeStampNow;
const then = factory.getNowInMilliSeconds;
cache.set(k0, newValue);
const metaAfter = cache.getMeta(k0);

Expand Down
10 changes: 4 additions & 6 deletions src/js/cache-max-stale-manager.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,14 +63,12 @@ class CacheMaxStaleManager {
}

#expireStaleCacheEntries() {
const NOW = this.#factory.utcTimeStampNow;

const NOW = this.#factory.getNowInMilliSeconds;
[...this.#cache.metaData]
.filter(it => it.lastAccessedAt + this.#maxStaleLifetimeMilliSeconds > NOW)
.map(it => it.key)
.filter(it => NOW - it.lastAccessedAt > this.#maxStaleLifetimeMilliSeconds)
.map(it => {
this.#cache.remove(it);
this.#logger.debug(`Expired key @ ${NOW}`);
this.#cache.remove(it.key);
this.#logger.debug(`Expired key ${it.key}`);
});
}

Expand Down
2 changes: 1 addition & 1 deletion src/js/cache.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ class Cache {
const value = this.#keyToValue.get(key);

this.#keyToMetaData.set(key, this.#factory.withCacheAccess(this.#keyToMetaData.get(key)));
this.#logger.debug(`<[${key}]`);
this.#logger.debug(`<[${key}=${value}]`);

return value;
} else {
Expand Down
15 changes: 6 additions & 9 deletions src/js/discord-interface.js
Original file line number Diff line number Diff line change
Expand Up @@ -98,25 +98,22 @@ class DiscordInterface {
/**
* @param {IdBotMessage} received
* @param {string} content
* @return Promise<Message>
*/
replyTo(received, content) {
async replyTo(received, content) {
this._logger.debug(`sending reply to ${received.id} with "${content}"`);
received.reply(content);
}

update = (message, content) => message
.edit(content)
.then(updatedMessage => this._logger.log(`Updated ${message.id} to "${updatedMessage.content}"`))
.catch(e => this._logger.error(`could not delete reply with id=${message.id}`, e));

return received.discordJsMessage.reply(content);
}

/**
* @param {Channel} channel
* @param messageId
*/
deleteMessage(channel, messageId) {
this._logger.debug(`attempting to delete replies to message with id=${messageId}`);
channel

return channel
.messages
.delete(messageId)
.then(() => this._logger.debug(`deleted reply with id=${messageId}`))
Expand Down
52 changes: 43 additions & 9 deletions src/js/factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import {GatewayIntentBits} from "discord-api-types/v10";
import {DiscordInterface} from "./discord-interface.js";
import {Application} from "./application.js";
import {CacheMaxStaleManager} from "./cache-max-stale-manager.js";
import {numberOfEmojiContained} from "./emoji.js";
import {isImageMediaType} from "./media-type.js";

const CLIENT_OPTIONS = {
intents: [
Expand All @@ -20,6 +22,16 @@ const CLIENT_OPTIONS = {

class Factory {

/**
* @type RegExp
*/
#MESSAGE_ID_REGEXP = /(?<=(^|\s|\W)ID:\s*)(\w+)(?!\WID:)/svg;

/**
* @type RegExp
*/
#CUSTOM_EMOJI_REGEX = /<(a)?:(?<name>\w+):(?<id>\d+)>/g;

static get #CLIENT_OPTIONS() {
return CLIENT_OPTIONS;
}
Expand All @@ -33,7 +45,8 @@ class Factory {
*/
createCacheExpirator(cache, tickIntervalDurationMilliSeconds = 100, maxStaleLifetimeMilliSeconds = undefined) {
return new CacheMaxStaleManager(
cache, this.createLogger("MaxStaleCacheManager"),
cache,
this.createLogger("MaxStaleCacheManager"),
this,
tickIntervalDurationMilliSeconds,
maxStaleLifetimeMilliSeconds
Expand Down Expand Up @@ -96,16 +109,37 @@ class Factory {
*
* @returns {number}
*/
get utcTimeStampNow() {
return new Date().getUTCMilliseconds();
get getNowInMilliSeconds() {
return Date.now();
}

/**
*
* @param discordJsMessage
* @returns {IdBotMessage}
*/
createIdBotMessage = discordJsMessage => new IdBotMessage(this, discordJsMessage);
createIdBotMessage = discordJsMessage => {
const content = discordJsMessage.content;

const contentCustomEmojiMatches = [...content.matchAll(this.#CUSTOM_EMOJI_REGEX)];

const contentStrippedOfCustomEmoji = content.replaceAll(this.#CUSTOM_EMOJI_REGEX, "");
const numberOfEmoji = numberOfEmojiContained(contentStrippedOfCustomEmoji);

const idMatches = [...content.matchAll(this.#MESSAGE_ID_REGEXP)];

const imageIdStats = this.createImageIdStats(
[...discordJsMessage.attachments.values()]
.map(v => v.contentType)
.filter(isImageMediaType)
.length,
numberOfEmoji,
contentCustomEmojiMatches.length,
idMatches.length
);

return new IdBotMessage(imageIdStats, discordJsMessage);
};

/**
* @param {string} [prefix]
Expand All @@ -119,23 +153,23 @@ class Factory {
* @returns {CacheMeta}
*/
createCacheMeta = key => {
const now = this.utcTimeStampNow;
const now = this.getNowInMilliSeconds;

return new CacheMeta(key, now, now);
};

withCacheAccess = cacheMeta => new CacheMeta(
cacheMeta.key,
cacheMeta.createdAt,
this.utcTimeStampNow,
this.getNowInMilliSeconds,
cacheMeta.lastUpdatedAt
);

withUpdate = cacheMeta => new CacheMeta(
cacheMeta.key,
cacheMeta.createdAt,
cacheMeta.lastAccessedAt,
this.utcTimeStampNow
this.getNowInMilliSeconds
);

/**
Expand Down
45 changes: 7 additions & 38 deletions src/js/id-bot-message.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,3 @@
import {isImageMediaType} from "./media-type.js";
import {numberOfEmojiContained} from "./emoji.js";

/**
* @typedef {typeof import('./image-id-stats.js')['ImageIdStats']} ImageIdStats
* @typedef {typeof import('discord.js')['Snowflake']} Snowflake
Expand All @@ -17,21 +14,16 @@ class IdBotMessage {
*/
#imageIdStats;

/**
* @type RegExp
*/
#MESSAGE_ID_REGEXP = /(?<=(^|\s|\W)ID:\s*)(\w+)(?!\WID:)/svg;

/**
* @type RegExp
*/
#CUSTOM_EMOJI_REGEX = /<(a)?:(?<name>\w+):(?<id>\d+)>/g;

/**
* @type Message
*/
#discordJsMessage;

get discordJsMessage() {
return this.#discordJsMessage;
}

get id() {
return this.#discordJsMessage.id;
}
Expand All @@ -49,12 +41,6 @@ class IdBotMessage {
return this.#discordJsMessage.content;
}

/**
* @param {string} content
*/
reply(content) {
return this.#discordJsMessage.reply(content);
}

/**
* @param {Snowflake} authorId
Expand Down Expand Up @@ -107,29 +93,12 @@ class IdBotMessage {
}

/**
* @param {Factory} factory
* @param {ImageIdStats} imageIdStats
* @param {Message} discordJsMessage the discord.js sourced discordJsMessage
*/
constructor(factory, discordJsMessage) {
constructor(imageIdStats, discordJsMessage) {
this.#discordJsMessage = discordJsMessage;

const content = discordJsMessage.content;
const contentCustomEmojiMatches = [...content.matchAll(this.#CUSTOM_EMOJI_REGEX)];

const contentStrippedOfCustomEmoji = content.replaceAll(this.#CUSTOM_EMOJI_REGEX, "");
const numberOfEmoji = numberOfEmojiContained(contentStrippedOfCustomEmoji);

const idMatches = [...content.matchAll(this.#MESSAGE_ID_REGEXP)];

this.#imageIdStats = factory.createImageIdStats(
[...discordJsMessage.attachments.values()]
.map(v => v.contentType)
.filter(isImageMediaType)
.length,
numberOfEmoji,
contentCustomEmojiMatches.length,
idMatches.length
);
this.#imageIdStats = imageIdStats;
}
}

Expand Down
21 changes: 12 additions & 9 deletions src/js/id-bot.js
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ class IdBot {
* @param {Channel} channel
* @param messageId
*/
#deleteChannelMessage(channel, messageId) {
async #deleteChannelMessage(channel, messageId) {
this.#logger.debug(`deleting channel message with id=${messageId}`);
this.#discordInterface.deleteMessage(channel, messageId);
}
Expand All @@ -53,11 +53,15 @@ class IdBot {
#deleteOurReplyTo(message) {
const messageId = message.id;
this.#logger.info(`deleting our reply to ${message.toIdString()}`);

const replyId = this.#cache.get(messageId);
if (replyId) {
this.#deleteChannelMessage(message.channel, replyId);
this.#cache.remove(messageId);
this.#logger.debug(this.#cache);
this
.#deleteChannelMessage(message.channel, replyId)
.then(() => {
this.#cache.remove(messageId);
this.#logger.debug(this.#cache);
});
} else {
this.#logger.warn(`${message.toIdString()} has no known replies`);
}
Expand All @@ -74,17 +78,16 @@ class IdBot {
* 1. if it is human authored and incorrectly identified, we post a reminder reply message
* 2. else, if it's a self-authored reply, we cache: m.referencedMessageId -> m.id
*
* Can we store arbitrary data in a message?
*
* @param {IdBotMessage} message
*
* @returns {Promise<void>}
* @returns {Promise<undefined>}
*/
#onMessageCreate = async message => {
this.#logger.debug(`new ${message}`);

if (message.isAuthorHuman) {
const imageIdStats = message.imageIdStats;

if (!imageIdStats.isCorrectlyIdentified) {
const reminderMessage = this.#reminderMessage(imageIdStats);
this.#logger.debug(
Expand Down Expand Up @@ -116,9 +119,9 @@ class IdBot {
const imageIdStats = updatedMessage.imageIdStats;

if (!imageIdStats.isCorrectlyIdentified) {
this.#deleteOurReplyTo(updatedMessage);
const replyMessageContent = this.#reminderMessage(imageIdStats);
this.#logger.debug(`${updatedMessage.toIdString()} not correctly identified, replying with "${replyMessageContent}"`);
this.#deleteOurReplyTo(updatedMessage);
this.#discordInterface.replyTo(updatedMessage, replyMessageContent);
} else {
this.#deleteOurReplyTo(updatedMessage);
Expand All @@ -133,7 +136,7 @@ class IdBot {
#onMessageDelete = async message => {
this.#logger.debug(`deleted id=${message.toIdString()}`);
if (message.isAuthorHuman) {
this.#logger.debug(`onMessageDelete: message author is human`);
this.#logger.debug(`deleted message's author is human`);
this.#deleteOurReplyTo(message);
}
};
Expand Down
Loading

0 comments on commit db1601d

Please sign in to comment.