mirror of
https://github.com/PR0M3TH3AN/bitvid.git
synced 2026-10-27 07:52:24 +00:00
refactor(comments): extract comment-cache subsystem (7/10)
File-size decomposition (no behavior change). Move the localStorage comment-cache methods (getCommentCacheKey, handleCommentCacheError, getCachedComments, cacheComments, removeCommentCache, clearCommentCache, persistCommentCache, serializeCommentsForCache) into js/services/commentThreadCache.js as functions that take the service instance; the service keeps thin delegators so all call sites are unchanged. Cache constants moved with them; FIVE_MINUTES_MS import dropped from the service. commentThreadService.js: 1296 -> 1100 lines (below its original 1122 baseline; re-recorded to 1100). Build + full unit suite green. Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
1 parent
87a29b8ff8
commit
c562a326ae
3 files changed
+276
-215
No files matched your search
@@ -0,0 +1,256 @@
|
||||
// js/services/commentThreadCache.js
|
||||
//
|
||||
// localStorage comment-cache subsystem extracted from commentThreadService.js to
|
||||
// keep that service under the file-size budget. Each function takes the service
|
||||
// instance (`ctx`) so behavior is identical to the former methods; the service
|
||||
// keeps thin delegators so existing call sites are unchanged. No behavior change.
|
||||
|
||||
import {
|
||||
FEATURE_IMPROVED_COMMENT_FETCHING,
|
||||
FIVE_MINUTES_MS,
|
||||
} from "../constants.js";
|
||||
import { normalizeHexId } from "../utils/hex.js";
|
||||
|
||||
const COMMENT_CACHE_PREFIX = "bitvid:comments:";
|
||||
const COMMENT_CACHE_TTL_MS = FIVE_MINUTES_MS;
|
||||
const COMMENT_CACHE_VERSION = 2;
|
||||
|
||||
function logDev(loggerCandidate, ...args) {
|
||||
if (loggerCandidate?.info) {
|
||||
loggerCandidate.info(...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCommentCacheKey(videoEventId) {
|
||||
const normalized = normalizeHexId(videoEventId);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${COMMENT_CACHE_PREFIX}${normalized.toLowerCase()}`;
|
||||
}
|
||||
|
||||
export function handleCommentCacheError(ctx, context, videoEventId, error) {
|
||||
ctx.commentCacheDiagnostics = {
|
||||
...ctx.commentCacheDiagnostics,
|
||||
storageUnavailable: true,
|
||||
};
|
||||
|
||||
const message =
|
||||
typeof videoEventId === "string" && videoEventId.trim()
|
||||
? `[commentThread] Failed to ${context} comment cache for ${videoEventId}.`
|
||||
: `[commentThread] Failed to ${context} comment cache.`;
|
||||
|
||||
if (ctx.logger?.user?.warn) {
|
||||
ctx.logger.user.warn(message, error);
|
||||
} else if (ctx.logger?.warn) {
|
||||
ctx.logger.warn(message, error);
|
||||
}
|
||||
|
||||
if (ctx.logger?.dev?.warn && ctx.logger.dev !== ctx.logger.user) {
|
||||
ctx.logger.dev.warn(message, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function getCachedComments(ctx, videoEventId) {
|
||||
if (
|
||||
!FEATURE_IMPROVED_COMMENT_FETCHING ||
|
||||
typeof localStorage === "undefined"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = ctx.getCommentCacheKey(videoEventId);
|
||||
if (!cacheKey) {
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
"[commentThread] Comment cache skipped: invalid video id.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let raw = null;
|
||||
try {
|
||||
raw = localStorage.getItem(cacheKey);
|
||||
} catch (error) {
|
||||
ctx.handleCommentCacheError("read", videoEventId, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw === null) {
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Comment cache miss for ${videoEventId}: no entry present.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: malformed payload.`,
|
||||
);
|
||||
ctx.removeCommentCache(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheVersion = Number.isFinite(parsed.version)
|
||||
? Number(parsed.version)
|
||||
: null;
|
||||
|
||||
if (cacheVersion !== COMMENT_CACHE_VERSION) {
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: version ${cacheVersion} != ${COMMENT_CACHE_VERSION}.`,
|
||||
);
|
||||
ctx.removeCommentCache(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
const comments = Array.isArray(parsed.comments)
|
||||
? parsed.comments
|
||||
: null;
|
||||
const timestamp = Number(parsed.timestamp);
|
||||
|
||||
if (
|
||||
Array.isArray(comments) &&
|
||||
Number.isFinite(timestamp) &&
|
||||
Date.now() - timestamp <= COMMENT_CACHE_TTL_MS
|
||||
) {
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Loaded ${comments.length} cached comments for ${videoEventId}.`,
|
||||
);
|
||||
return comments;
|
||||
}
|
||||
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: entry expired.`,
|
||||
);
|
||||
ctx.removeCommentCache(cacheKey);
|
||||
} catch (error) {
|
||||
if (ctx.logger?.warn) {
|
||||
ctx.logger.warn(
|
||||
`[commentThread] Failed to parse cached comments for ${videoEventId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: parse error.`,
|
||||
);
|
||||
ctx.removeCommentCache(cacheKey);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function cacheComments(ctx, videoEventId, comments) {
|
||||
if (
|
||||
!FEATURE_IMPROVED_COMMENT_FETCHING ||
|
||||
typeof localStorage === "undefined" ||
|
||||
!Array.isArray(comments)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = ctx.getCommentCacheKey(videoEventId);
|
||||
if (!cacheKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
cacheKey,
|
||||
JSON.stringify({
|
||||
version: COMMENT_CACHE_VERSION,
|
||||
comments,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
logDev(
|
||||
ctx.logger?.dev,
|
||||
`[commentThread] Cached ${comments.length} comments for ${videoEventId}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
ctx.handleCommentCacheError("write", videoEventId, error);
|
||||
}
|
||||
}
|
||||
|
||||
export function removeCommentCache(ctx, cacheKey) {
|
||||
if (!cacheKey || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(cacheKey);
|
||||
} catch (error) {
|
||||
if (ctx.logger?.warn) {
|
||||
ctx.logger.warn(
|
||||
`[commentThread] Failed to clear cached comments for ${cacheKey}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function clearCommentCache(ctx, videoEventId = null) {
|
||||
if (!FEATURE_IMPROVED_COMMENT_FETCHING || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoEventId) {
|
||||
ctx.removeCommentCache(ctx.getCommentCacheKey(videoEventId));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys
|
||||
.filter((key) => key.startsWith(COMMENT_CACHE_PREFIX))
|
||||
.forEach((key) => ctx.removeCommentCache(key));
|
||||
} catch (error) {
|
||||
if (ctx.logger?.warn) {
|
||||
ctx.logger.warn(
|
||||
"[commentThread] Failed to clear comment cache:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function persistCommentCache(ctx) {
|
||||
if (!FEATURE_IMPROVED_COMMENT_FETCHING || !ctx.videoEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = ctx.serializeCommentsForCache();
|
||||
ctx.cacheComments(ctx.videoEventId, comments);
|
||||
}
|
||||
|
||||
export function serializeCommentsForCache(ctx) {
|
||||
const events = Array.from(ctx.eventsById.values());
|
||||
return events.sort((a, b) => {
|
||||
const aTime = Number.isFinite(a?.created_at) ? a.created_at : 0;
|
||||
const bTime = Number.isFinite(b?.created_at) ? b.created_at : 0;
|
||||
if (aTime !== bTime) {
|
||||
return aTime - bTime;
|
||||
}
|
||||
|
||||
const aId = normalizeHexId(a?.id);
|
||||
const bId = normalizeHexId(b?.id);
|
||||
if (aId && bId) {
|
||||
return aId.localeCompare(bId);
|
||||
}
|
||||
if (aId) {
|
||||
return -1;
|
||||
}
|
||||
if (bId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
@@ -4,16 +4,23 @@ import logger, { devLogger, userLogger } from "../utils/logger.js";
|
||||
import { normalizeHexId, normalizeHexPubkey } from "../utils/hex.js";
|
||||
import { buildVideoAddressPointer } from "../utils/videoPointer.js";
|
||||
import { COMMENT_EVENT_KIND } from "../nostr/commentEvents.js";
|
||||
import { FEATURE_IMPROVED_COMMENT_FETCHING, FIVE_MINUTES_MS } from "../constants.js";
|
||||
import { FEATURE_IMPROVED_COMMENT_FETCHING } from "../constants.js";
|
||||
import {
|
||||
getCommentCacheKey as cacheGetCommentCacheKey,
|
||||
handleCommentCacheError as cacheHandleCommentCacheError,
|
||||
getCachedComments as cacheGetCachedComments,
|
||||
cacheComments as cacheCacheComments,
|
||||
removeCommentCache as cacheRemoveCommentCache,
|
||||
clearCommentCache as cacheClearCommentCache,
|
||||
persistCommentCache as cachePersistCommentCache,
|
||||
serializeCommentsForCache as cacheSerializeCommentsForCache,
|
||||
} from "./commentThreadCache.js";
|
||||
|
||||
const ROOT_PARENT_KEY = "__root__";
|
||||
const DEFAULT_INITIAL_LIMIT = 40;
|
||||
const DEFAULT_HYDRATION_DEBOUNCE_MS = 25;
|
||||
const PROFILE_FETCH_MAX_ATTEMPTS = 3;
|
||||
const PROFILE_FETCH_BACKOFF_MS = 50;
|
||||
const COMMENT_CACHE_PREFIX = "bitvid:comments:";
|
||||
const COMMENT_CACHE_TTL_MS = FIVE_MINUTES_MS;
|
||||
const COMMENT_CACHE_VERSION = 2;
|
||||
|
||||
function toPositiveInteger(value, fallback) {
|
||||
const numeric = Number(value);
|
||||
@@ -400,237 +407,35 @@ export default class CommentThreadService {
|
||||
}
|
||||
|
||||
getCommentCacheKey(videoEventId) {
|
||||
const normalized = normalizeHexId(videoEventId);
|
||||
if (!normalized) {
|
||||
return "";
|
||||
}
|
||||
|
||||
return `${COMMENT_CACHE_PREFIX}${normalized.toLowerCase()}`;
|
||||
return cacheGetCommentCacheKey(videoEventId);
|
||||
}
|
||||
|
||||
handleCommentCacheError(context, videoEventId, error) {
|
||||
this.commentCacheDiagnostics = {
|
||||
...this.commentCacheDiagnostics,
|
||||
storageUnavailable: true,
|
||||
};
|
||||
|
||||
const message =
|
||||
typeof videoEventId === "string" && videoEventId.trim()
|
||||
? `[commentThread] Failed to ${context} comment cache for ${videoEventId}.`
|
||||
: `[commentThread] Failed to ${context} comment cache.`;
|
||||
|
||||
if (this.logger?.user?.warn) {
|
||||
this.logger.user.warn(message, error);
|
||||
} else if (this.logger?.warn) {
|
||||
this.logger.warn(message, error);
|
||||
}
|
||||
|
||||
if (this.logger?.dev?.warn && this.logger.dev !== this.logger.user) {
|
||||
this.logger.dev.warn(message, error);
|
||||
}
|
||||
return cacheHandleCommentCacheError(this, context, videoEventId, error);
|
||||
}
|
||||
|
||||
getCachedComments(videoEventId) {
|
||||
if (
|
||||
!FEATURE_IMPROVED_COMMENT_FETCHING ||
|
||||
typeof localStorage === "undefined"
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheKey = this.getCommentCacheKey(videoEventId);
|
||||
if (!cacheKey) {
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
"[commentThread] Comment cache skipped: invalid video id.",
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
let raw = null;
|
||||
try {
|
||||
raw = localStorage.getItem(cacheKey);
|
||||
} catch (error) {
|
||||
this.handleCommentCacheError("read", videoEventId, error);
|
||||
return null;
|
||||
}
|
||||
|
||||
if (raw === null) {
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Comment cache miss for ${videoEventId}: no entry present.`,
|
||||
);
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(raw);
|
||||
if (!parsed || typeof parsed !== "object") {
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: malformed payload.`,
|
||||
);
|
||||
this.removeCommentCache(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
const cacheVersion = Number.isFinite(parsed.version)
|
||||
? Number(parsed.version)
|
||||
: null;
|
||||
|
||||
if (cacheVersion !== COMMENT_CACHE_VERSION) {
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: version ${cacheVersion} != ${COMMENT_CACHE_VERSION}.`,
|
||||
);
|
||||
this.removeCommentCache(cacheKey);
|
||||
return null;
|
||||
}
|
||||
|
||||
const comments = Array.isArray(parsed.comments)
|
||||
? parsed.comments
|
||||
: null;
|
||||
const timestamp = Number(parsed.timestamp);
|
||||
|
||||
if (
|
||||
Array.isArray(comments) &&
|
||||
Number.isFinite(timestamp) &&
|
||||
Date.now() - timestamp <= COMMENT_CACHE_TTL_MS
|
||||
) {
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Loaded ${comments.length} cached comments for ${videoEventId}.`,
|
||||
);
|
||||
return comments;
|
||||
}
|
||||
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: entry expired.`,
|
||||
);
|
||||
this.removeCommentCache(cacheKey);
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn(
|
||||
`[commentThread] Failed to parse cached comments for ${videoEventId}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Comment cache rejected for ${videoEventId}: parse error.`,
|
||||
);
|
||||
this.removeCommentCache(cacheKey);
|
||||
}
|
||||
|
||||
return null;
|
||||
return cacheGetCachedComments(this, videoEventId);
|
||||
}
|
||||
|
||||
cacheComments(videoEventId, comments) {
|
||||
if (
|
||||
!FEATURE_IMPROVED_COMMENT_FETCHING ||
|
||||
typeof localStorage === "undefined" ||
|
||||
!Array.isArray(comments)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
const cacheKey = this.getCommentCacheKey(videoEventId);
|
||||
if (!cacheKey) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.setItem(
|
||||
cacheKey,
|
||||
JSON.stringify({
|
||||
version: COMMENT_CACHE_VERSION,
|
||||
comments,
|
||||
timestamp: Date.now(),
|
||||
}),
|
||||
);
|
||||
logDev(
|
||||
this.logger?.dev,
|
||||
`[commentThread] Cached ${comments.length} comments for ${videoEventId}.`,
|
||||
);
|
||||
} catch (error) {
|
||||
this.handleCommentCacheError("write", videoEventId, error);
|
||||
}
|
||||
return cacheCacheComments(this, videoEventId, comments);
|
||||
}
|
||||
|
||||
removeCommentCache(cacheKey) {
|
||||
if (!cacheKey || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
localStorage.removeItem(cacheKey);
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn(
|
||||
`[commentThread] Failed to clear cached comments for ${cacheKey}:`,
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return cacheRemoveCommentCache(this, cacheKey);
|
||||
}
|
||||
|
||||
clearCommentCache(videoEventId = null) {
|
||||
if (!FEATURE_IMPROVED_COMMENT_FETCHING || typeof localStorage === "undefined") {
|
||||
return;
|
||||
}
|
||||
|
||||
if (videoEventId) {
|
||||
this.removeCommentCache(this.getCommentCacheKey(videoEventId));
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const keys = Object.keys(localStorage);
|
||||
keys
|
||||
.filter((key) => key.startsWith(COMMENT_CACHE_PREFIX))
|
||||
.forEach((key) => this.removeCommentCache(key));
|
||||
} catch (error) {
|
||||
if (this.logger?.warn) {
|
||||
this.logger.warn(
|
||||
"[commentThread] Failed to clear comment cache:",
|
||||
error,
|
||||
);
|
||||
}
|
||||
}
|
||||
return cacheClearCommentCache(this, videoEventId);
|
||||
}
|
||||
|
||||
persistCommentCache() {
|
||||
if (!FEATURE_IMPROVED_COMMENT_FETCHING || !this.videoEventId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const comments = this.serializeCommentsForCache();
|
||||
this.cacheComments(this.videoEventId, comments);
|
||||
return cachePersistCommentCache(this);
|
||||
}
|
||||
|
||||
serializeCommentsForCache() {
|
||||
const events = Array.from(this.eventsById.values());
|
||||
return events.sort((a, b) => {
|
||||
const aTime = Number.isFinite(a?.created_at) ? a.created_at : 0;
|
||||
const bTime = Number.isFinite(b?.created_at) ? b.created_at : 0;
|
||||
if (aTime !== bTime) {
|
||||
return aTime - bTime;
|
||||
}
|
||||
|
||||
const aId = normalizeHexId(a?.id);
|
||||
const bId = normalizeHexId(b?.id);
|
||||
if (aId && bId) {
|
||||
return aId.localeCompare(bId);
|
||||
}
|
||||
if (aId) {
|
||||
return -1;
|
||||
}
|
||||
if (bId) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
return cacheSerializeCommentsForCache(this);
|
||||
}
|
||||
|
||||
startSubscription(target, options = {}) {
|
||||
|
||||
@@ -64,7 +64,7 @@ const GRANDFATHERED = {
|
||||
"js/services/r2Service.js": 1194,
|
||||
"js/ui/applicationBootstrap.js": 1163,
|
||||
"js/ui/components/EditModal.js": 1130,
|
||||
"js/services/commentThreadService.js": 1122,
|
||||
"js/services/commentThreadService.js": 1100,
|
||||
"js/nostr/managers/SignerManager.js": 1106,
|
||||
"js/nostr/nip46Connector.js": 1094,
|
||||
"js/ui/overlay/popoverEngine.js": 1070,
|
||||
|
||||
Reference in new issue
Block a user