@replyke/express
Version:
Replyke: Build interactive apps with social features like comments, votes, feeds, user lists, notifications, and more.
93 lines (92 loc) • 4.1 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.default = scoreEntity;
const date_fns_1 = require("date-fns");
const validateEntityDataForScoring = (entity) => {
// Check `scoreUpdatedAt` validity
if (!entity.scoreUpdatedAt) {
console.error("Missing `scoreUpdatedAt` in entity.");
throw new Error("Invalid entity: `scoreUpdatedAt` is required.");
}
// Check `createdAt` validity
if (!entity.createdAt) {
console.error("Missing or invalid `createdAt` in entity.");
throw new Error("Invalid entity: `createdAt` is required.");
}
// Validate that `createdAt` is not in the future
if (new Date(entity.createdAt) > new Date()) {
console.error("`createdAt` is in the future:");
throw new Error("Invalid entity: `createdAt` cannot be in the future.");
}
// Validate numerical fields
if (!Array.isArray(entity.upvotes)) {
console.error("Invalid `upvotes` field:", entity.upvotes);
throw new Error("Invalid entity: `upvotes` must be an array.");
}
if (!Array.isArray(entity.downvotes)) {
console.error("Invalid `downvotes` field:", entity.downvotes);
throw new Error("Invalid entity: `downvotes` must be an array.");
}
if (typeof entity.repliesCount !== "number") {
console.error("Invalid `repliesCount` field:", entity.repliesCount);
throw new Error("Invalid entity: `repliesCount` must be a number.");
}
if (typeof entity.sharesCount !== "number") {
console.error("Invalid `sharesCount` field:", entity.sharesCount);
throw new Error("Invalid entity: `sharesCount` must be a number.");
}
// Validate `views`
if (typeof entity.views !== "number" || entity.views <= 0) {
console.error("Invalid `views` field:", entity.views);
throw new Error("Invalid entity: `views` must be a positive number.");
}
};
const GAP_IN_MINUTES_BETWEEN_RESCORING = 5;
function scoreEntity(entity) {
validateEntityDataForScoring(entity);
const currentStateReturn = {
updated: false,
newScore: entity.score,
newScoreUpdatedAt: entity.scoreUpdatedAt,
};
// Validate `scoreUpdatedAt`
if (entity.scoreUpdatedAt &&
(0, date_fns_1.differenceInMinutes)(new Date(), new Date(entity.scoreUpdatedAt)) <=
GAP_IN_MINUTES_BETWEEN_RESCORING) {
return currentStateReturn;
}
const upvotesCount = entity.upvotes.length;
const downvotesCount = entity.downvotes.length;
const repliesCount = entity.repliesCount;
const sharesCount = entity.sharesCount;
const views = Math.max(entity.views, 1); // Ensure views is at least 1
const createdAt = entity.createdAt;
// Weight factors for different elements of the score
const UPVOTE_WEIGHT = 1;
const DOWNVOTE_WEIGHT = -0.5;
const REPLY_WEIGHT = 0.5;
const SHARE_WEIGHT = 2;
// Base interaction score
const interactionScore = UPVOTE_WEIGHT * upvotesCount +
DOWNVOTE_WEIGHT * downvotesCount +
REPLY_WEIGHT * repliesCount +
SHARE_WEIGHT * sharesCount;
// Base score for all posts, decays over time
const BASE_SCORE = 2; // Starting score for new posts
const HALF_LIFE = 48; // Half-life in hours for the base score decay
// Calculate time since publication in hours
const time_since_publication = (new Date().getTime() - new Date(createdAt).getTime()) / (3600 * 1000);
// Calculate decay factor for the base score
const base_decay = Math.exp(-time_since_publication / HALF_LIFE);
// Adjust the base score with decay
const baseAdjustedScore = BASE_SCORE * base_decay;
// Adjust interaction score by views to normalize for exposure
const exposureAdjustedScore = interactionScore / Math.log2(views + 1);
// Combine base score and interaction score
const final_score = Math.max(0, baseAdjustedScore + exposureAdjustedScore);
return {
updated: true,
newScore: parseFloat(final_score.toFixed(2)),
newScoreUpdatedAt: new Date(),
};
}