Files
2026-07-13 12:20:32 +08:00

99 lines
2.9 KiB
JavaScript

/**
* UserMetaCache
*
* Singleton in-memory cache for per-user browser metadata (timezone, language).
* Keyed by numeric user ID in multi-user mode, or the string 'primary' in
* single-user mode. Populated from the X-Timezone / X-Language request headers
* on every authenticated HTTP request via the validatedRequest middleware.
*
* Because every request repopulates the cache from headers, the data stays
* fresh within a session without any database writes.
*
* Usage:
* const { UserMetaCache } = require("../userLocale");
* UserMetaCache.set(userId, { timezone, lang });
* const { timezone, lang } = UserMetaCache.get(userId);
*/
const DEFAULT_TIMEZONE = "UTC";
const DEFAULT_LANG = "en";
class _UserMetaCache {
#cache = new Map();
/**
* Validate that a string is a recognised IANA timezone identifier.
* @param {string} tz
* @returns {boolean}
*/
#isValidTimezone(tz) {
if (!tz || typeof tz !== "string") return false;
try {
Intl.DateTimeFormat(undefined, { timeZone: tz });
return true;
} catch {
return false;
}
}
/**
* Normalise a user ID to a cache key.
* @param {number|null|undefined} userId
* @returns {number|'primary'}
*/
#key(userId) {
return userId ?? "primary";
}
/**
* Store or update locale metadata for a user.
* Invalid timezone strings are silently ignored (existing/default value kept).
*
* @param {number|null} userId - Numeric user ID, or null for single-user mode.
* @param {{ timezone?: string, lang?: string }} meta
*/
set(userId, { timezone, lang } = {}) {
const key = this.#key(userId);
const current = this.#cache.get(key) || {};
this.#cache.set(key, {
timezone:
timezone && this.#isValidTimezone(timezone)
? timezone
: current.timezone || DEFAULT_TIMEZONE,
lang: lang || current.lang || DEFAULT_LANG,
});
}
/**
* Retrieve locale metadata for a user.
* Returns defaults if nothing has been cached yet.
*
* @param {number|null} userId - Numeric user ID, or null for single-user mode.
* @returns {{ timezone: string, lang: string }}
*/
get(userId) {
return (
this.#cache.get(this.#key(userId)) || {
timezone: DEFAULT_TIMEZONE,
lang: DEFAULT_LANG,
}
);
}
/**
* Read X-Timezone and X-Language from an Express request and store them.
* Intended to be called from the validatedRequest middleware after auth.
*
* @param {import('express').Request} request
* @param {number|null} userId - null for single-user mode (stored under 'primary')
*/
setFromRequest(request, userId = null) {
const timezone = request.header("X-Timezone");
const lang = request.header("X-Language");
if (timezone || lang) this.set(userId, { timezone, lang });
}
}
const UserMetaCache = new _UserMetaCache();
module.exports = { UserMetaCache };