pixel/js/rotation-policy.js
2026-06-02 17:19:36 +09:00

76 lines
2.7 KiB
JavaScript

(function () {
'use strict';
const root = window.PixelIslandModules ||= {};
const utils = root.CoreUtils || {};
const fnv1a = utils.fnv1a || ((value) => {
let hash = 0x811c9dc5;
for (const ch of String(value)) {
hash ^= ch.charCodeAt(0);
hash = Math.imul(hash, 0x01000193);
}
return (hash >>> 0).toString(16).padStart(8, '0');
});
const DEFAULT_CONFIG = {
defaultDisplayLimit: 250,
newArrivalSlots: 150,
revivalSlots: 100,
publishLimitFirstDay: 5,
publishLimitTrusted: 10,
upvoteDelaySlots: 20,
downvoteAdvanceSlots: 25,
upvoteRankCap: 50
};
function objectPublicAt(object) {
return Number(object?.publishedAt || object?.placedAt || object?.createdAt || Date.now());
}
function entry(kind, object, baseIndex, votes, config = DEFAULT_CONFIG) {
const rawUp = Number(votes?.up) || 0;
const up = Math.min(rawUp, config.upvoteRankCap);
const down = Number(votes?.down) || 0;
return {
kind,
object,
id: object?.id,
assetId: object?.assetId,
publicAt: objectPublicAt(object),
baseIndex,
up,
rawUp,
down,
effectiveSlot: baseIndex + up * config.upvoteDelaySlots - down * config.downvoteAdvanceSlots
};
}
function seededScore(id, salt, rotationAt = Date.now()) {
const day = Math.floor((rotationAt || Date.now()) / (24 * 60 * 60 * 1000));
return parseInt(fnv1a(`${id}|${salt}|${day}`).slice(0, 8), 16) / 0xffffffff;
}
function buckets(entries, localLimit, config = DEFAULT_CONFIG, rotationAt = Date.now()) {
const newCap = Math.min(config.newArrivalSlots, localLimit);
const revivalCap = Math.max(0, Math.min(config.revivalSlots, localLimit - newCap));
const newest = entries
.slice()
.sort((a, b) => b.effectiveSlot - a.effectiveSlot || b.publicAt - a.publicAt || String(b.id).localeCompare(String(a.id)))
.slice(0, newCap);
const newestIds = new Set(newest.map((item) => item.id));
const revival = entries
.filter((item) => !newestIds.has(item.id))
.sort((a, b) => seededScore(b.id, 'revival', rotationAt) - seededScore(a.id, 'revival', rotationAt) || b.up - a.up || String(b.id).localeCompare(String(a.id)))
.slice(0, revivalCap);
return { newest, revival, entries, visibleIds: new Set([...newest, ...revival].map((item) => item.id)) };
}
function publishLimit(account, now = Date.now(), config = DEFAULT_CONFIG) {
if (!account?.createdAt) return 0;
return now - Number(account.createdAt) < 24 * 60 * 60 * 1000
? config.publishLimitFirstDay
: config.publishLimitTrusted;
}
root.RotationPolicy = { DEFAULT_CONFIG, objectPublicAt, entry, buckets, seededScore, publishLimit };
})();