@oxy.so/core 1.1.1 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/esm/index.js CHANGED
@@ -91,7 +91,7 @@ export { TopicType, TopicSource } from './models/Topic.js';
91
91
  // ---------------------------------------------------------------------------
92
92
  // Languages
93
93
  // ---------------------------------------------------------------------------
94
- export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, } from './utils/languageUtils.js';
94
+ export { SUPPORTED_LANGUAGES, FALLBACK_LOCALE, getBaseLanguage, normalizeLocale, isSupportedLocale, getLanguageMetadata, getLanguageName, getNativeLanguageName, isRTLLocale, getUserLanguages, getPrimaryLanguage, coerceToSupportedLocale, } from './utils/languageUtils.js';
95
95
  // ---------------------------------------------------------------------------
96
96
  // Platform detection
97
97
  // ---------------------------------------------------------------------------
@@ -78,6 +78,8 @@ export class TTLCache {
78
78
  const effectiveTTL = ttl === undefined ? this.defaultTTL : ttl;
79
79
  const expiresAt = now + effectiveTTL;
80
80
  this.cache.set(key, { data, timestamp: now, expiresAt });
81
+ if (!cleanupInterval && activeCaches.has(this))
82
+ updateCleanupInterval();
81
83
  }
82
84
  /**
83
85
  * Delete a specific cache entry
@@ -85,7 +87,10 @@ export class TTLCache {
85
87
  * @returns true if entry was deleted, false if not found
86
88
  */
87
89
  delete(key) {
88
- return this.cache.delete(key);
90
+ const deleted = this.cache.delete(key);
91
+ if (activeCaches.has(this))
92
+ updateCleanupInterval();
93
+ return deleted;
89
94
  }
90
95
  /**
91
96
  * Clear all cache entries
@@ -94,6 +99,8 @@ export class TTLCache {
94
99
  this.cache.clear();
95
100
  this.hits = 0;
96
101
  this.misses = 0;
102
+ if (activeCaches.has(this))
103
+ updateCleanupInterval();
97
104
  }
98
105
  /**
99
106
  * Check if a key exists and is not expired
@@ -191,28 +198,30 @@ const activeCaches = new Set();
191
198
  */
192
199
  export function registerCacheForCleanup(cache) {
193
200
  activeCaches.add(cache);
194
- // Start cleanup interval if not already running
195
- if (!cleanupInterval) {
196
- cleanupInterval = setInterval(() => {
197
- for (const cache of activeCaches) {
198
- cache.cleanup();
199
- }
200
- }, 60000); // Every minute
201
- // Never block process/worker exit on a housekeeping timer.
202
- cleanupInterval.unref?.();
201
+ updateCleanupInterval();
202
+ }
203
+ /** Empty registered caches need no timer; importing a client must stay inert. */
204
+ function updateCleanupInterval() {
205
+ const hasEntries = [...activeCaches].some(cache => cache.size() > 0);
206
+ if (!hasEntries) {
207
+ if (cleanupInterval)
208
+ clearInterval(cleanupInterval);
209
+ cleanupInterval = null;
210
+ return;
203
211
  }
212
+ if (cleanupInterval)
213
+ return;
214
+ cleanupInterval = setInterval(() => {
215
+ for (const cache of activeCaches)
216
+ cache.cleanup();
217
+ updateCleanupInterval();
218
+ }, 60000);
219
+ cleanupInterval.unref?.();
204
220
  }
205
- /**
206
- * Unregister a cache from automatic cleanup
207
- * @param cache Cache instance to unregister
208
- */
221
+ /** Unregister a cache without changing its entries or TTL behavior. */
209
222
  export function unregisterCacheFromCleanup(cache) {
210
223
  activeCaches.delete(cache);
211
- // Stop cleanup interval if no caches are registered
212
- if (activeCaches.size === 0 && cleanupInterval) {
213
- clearInterval(cleanupInterval);
214
- cleanupInterval = null;
215
- }
224
+ updateCleanupInterval();
216
225
  }
217
226
  /**
218
227
  * Stop all cleanup intervals (useful for testing)
@@ -210,3 +210,28 @@ export function getUserLanguages(user) {
210
210
  export function getPrimaryLanguage(user) {
211
211
  return getUserLanguages(user)[0];
212
212
  }
213
+ /**
214
+ * Coerce a locale down to one from a CALLER-provided catalog: an exact
215
+ * canonical match wins, else the closest entry sharing the same base
216
+ * language (an `es-MX` account reads a host app's `es-ES` catalog), else
217
+ * `fallback`.
218
+ *
219
+ * A host app's own shipped translation catalog is rarely {@link SUPPORTED_LANGUAGES}
220
+ * itself — Oxy's account-locale catalog is broader than any one app's
221
+ * translated strings — so this is the one place that gap is bridged. Every Oxy
222
+ * app was independently re-deriving this exact algorithm against its own
223
+ * catalog before it moved here.
224
+ */
225
+ export function coerceToSupportedLocale(locale, supportedLocales, fallback) {
226
+ if (locale) {
227
+ const canonical = normalizeLocale(locale);
228
+ if (canonical && supportedLocales.includes(canonical)) {
229
+ return canonical;
230
+ }
231
+ const base = getBaseLanguage(locale);
232
+ const byBase = supportedLocales.find((entry) => getBaseLanguage(entry) === base);
233
+ if (byBase)
234
+ return byBase;
235
+ }
236
+ return fallback;
237
+ }