gt-react 11.1.0 → 11.1.1
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/CHANGELOG.md +11 -0
- package/dist/LocalStorageTranslationCache-B66Ni5IM.mjs +192 -0
- package/dist/LocalStorageTranslationCache-B66Ni5IM.mjs.map +1 -0
- package/dist/LocalStorageTranslationCache-DOEJz82p.cjs +192 -0
- package/dist/LocalStorageTranslationCache-DOEJz82p.cjs.map +1 -0
- package/dist/index.client.cjs +13 -215
- package/dist/index.client.cjs.map +1 -1
- package/dist/index.client.d.cts.map +1 -1
- package/dist/index.client.d.mts.map +1 -1
- package/dist/index.client.mjs +13 -215
- package/dist/index.client.mjs.map +1 -1
- package/dist/index.server.d.cts.map +1 -1
- package/dist/index.server.d.mts.map +1 -1
- package/dist/index.types.d.cts.map +1 -1
- package/dist/index.types.d.mts.map +1 -1
- package/package.json +4 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# gt-react
|
|
2
2
|
|
|
3
|
+
## 11.1.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [#1987](https://github.com/generaltranslation/gt/pull/1987) [`132f34f`](https://github.com/generaltranslation/gt/commit/132f34f6512b0fd91b0bc33ef89df0896e41b1fc) Thanks [@bgub](https://github.com/bgub)! - Lazy-load the development-only localStorage translation cache from the browser entrypoint.
|
|
8
|
+
|
|
9
|
+
- Updated dependencies [[`29cd6b8`](https://github.com/generaltranslation/gt/commit/29cd6b89f3587d3253cfadde6bec925d8697324b)]:
|
|
10
|
+
- generaltranslation@9.0.5
|
|
11
|
+
- gt-i18n@1.0.9
|
|
12
|
+
- @generaltranslation/react-core@11.1.1
|
|
13
|
+
|
|
3
14
|
## 11.1.0
|
|
4
15
|
|
|
5
16
|
### Patch Changes
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//#region src/i18n-cache/LocalStorageTranslationCache.ts
|
|
2
|
+
const STORAGE_KEY_PREFIX = "gt:tx:";
|
|
3
|
+
const PURGE_TIMESTAMP_PREFIX = "gt:tx:purge:";
|
|
4
|
+
const FLUSH_INTERVAL = 500;
|
|
5
|
+
const DEFAULT_MAX_SIZE = 1e6;
|
|
6
|
+
const DEFAULT_TTL_MS = 864e5;
|
|
7
|
+
const DEFAULT_PURGE_INTERVAL_MS = 3e5;
|
|
8
|
+
const PURGE_TARGET_RATIO = .8;
|
|
9
|
+
const activeIntervals = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* A localStorage-backed translation cache for a single locale.
|
|
12
|
+
* Used in development mode only to persist runtime translations across page refreshes.
|
|
13
|
+
*
|
|
14
|
+
* Entries are stored with per-entry expiry timestamps and the cache is purged
|
|
15
|
+
* when estimated size exceeds the configured maximum.
|
|
16
|
+
*/
|
|
17
|
+
var LocalStorageTranslationCache = class {
|
|
18
|
+
/**
|
|
19
|
+
* @param locale - The locale this cache is for
|
|
20
|
+
* @param projectId - The project id (namespaces localStorage keys)
|
|
21
|
+
* @param init - Optional initial translations to merge on top of localStorage data.
|
|
22
|
+
* init values take priority over stale localStorage entries.
|
|
23
|
+
* @param maxSize - Maximum cache size in characters (default: ~1M)
|
|
24
|
+
* @param ttl - TTL in milliseconds for each entry (default: 24 hours)
|
|
25
|
+
* @param purgeInterval - Background purge check interval in ms (default: 5 min)
|
|
26
|
+
*/
|
|
27
|
+
constructor({ locale, projectId, init, maxSize, ttl, purgeInterval }) {
|
|
28
|
+
this._writeBuffer = {};
|
|
29
|
+
this._flushTimer = null;
|
|
30
|
+
this._estimatedSize = 0;
|
|
31
|
+
this._storageKey = `${STORAGE_KEY_PREFIX}${projectId}:${locale}`;
|
|
32
|
+
this._purgeTimestampKey = `${PURGE_TIMESTAMP_PREFIX}${projectId}:${locale}`;
|
|
33
|
+
this._maxSize = maxSize ?? DEFAULT_MAX_SIZE;
|
|
34
|
+
this._ttl = ttl ?? DEFAULT_TTL_MS;
|
|
35
|
+
this._purgeInterval = purgeInterval ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
36
|
+
if (init) this.initStorage(init);
|
|
37
|
+
if (activeIntervals.has(this._storageKey)) clearInterval(activeIntervals.get(this._storageKey));
|
|
38
|
+
const intervalId = setInterval(() => this._backgroundPurge(), this._purgeInterval);
|
|
39
|
+
activeIntervals.set(this._storageKey, intervalId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the full translation map (cache + pending buffer writes).
|
|
43
|
+
* Filters out expired entries. Buffer entries take priority.
|
|
44
|
+
*/
|
|
45
|
+
getInternalCache() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const cache = this._readFromStorage();
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [key, entry] of Object.entries(cache)) if (entry.exp > now) result[key] = entry.t;
|
|
50
|
+
Object.assign(result, this._writeBuffer);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Queue a translation for writing to localStorage.
|
|
55
|
+
* Writes are batched via a debounced flush.
|
|
56
|
+
*/
|
|
57
|
+
write(hash, translation) {
|
|
58
|
+
this._writeBuffer[hash] = translation;
|
|
59
|
+
this._scheduleFlush();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Remove specific entries from the cache by hash.
|
|
63
|
+
*/
|
|
64
|
+
purge(hashes) {
|
|
65
|
+
const cache = this._readFromStorage();
|
|
66
|
+
for (const hash of hashes) delete cache[hash];
|
|
67
|
+
this._writeRaw(JSON.stringify(cache));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Schedule a flush of the write buffer.
|
|
71
|
+
* Uses a leading throttle — the first write in a burst schedules a flush
|
|
72
|
+
* after FLUSH_INTERVAL ms; subsequent writes before the timer fires are
|
|
73
|
+
* batched into the same flush.
|
|
74
|
+
*/
|
|
75
|
+
_scheduleFlush() {
|
|
76
|
+
if (this._flushTimer) return;
|
|
77
|
+
this._flushTimer = setTimeout(() => {
|
|
78
|
+
this._flushTimer = null;
|
|
79
|
+
this._flush();
|
|
80
|
+
}, FLUSH_INTERVAL);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Merge the write buffer into the cache and persist to localStorage.
|
|
84
|
+
* Purges before writing if estimated size exceeds max.
|
|
85
|
+
*/
|
|
86
|
+
_flush() {
|
|
87
|
+
if (Object.keys(this._writeBuffer).length === 0) return;
|
|
88
|
+
try {
|
|
89
|
+
const cache = this._readFromStorage();
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (this._estimatedSize > this._maxSize) this._purgeCache(cache, now);
|
|
92
|
+
const exp = now + this._ttl;
|
|
93
|
+
for (const [key, value] of Object.entries(this._writeBuffer)) cache[key] = {
|
|
94
|
+
t: value,
|
|
95
|
+
exp
|
|
96
|
+
};
|
|
97
|
+
this._writeRaw(JSON.stringify(cache));
|
|
98
|
+
} catch {}
|
|
99
|
+
this._writeBuffer = {};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Purge entries from the cache in place.
|
|
103
|
+
* Phase 1: Remove expired entries.
|
|
104
|
+
* Phase 2: If still over target, drop oldest entries by expiry time.
|
|
105
|
+
*/
|
|
106
|
+
_purgeCache(cache, now) {
|
|
107
|
+
const keysBeforePurge = Object.keys(cache);
|
|
108
|
+
if (keysBeforePurge.length === 0) return;
|
|
109
|
+
const avgEntrySize = this._estimatedSize / keysBeforePurge.length;
|
|
110
|
+
deleteExpiredEntries(cache, now);
|
|
111
|
+
const targetSize = this._maxSize * PURGE_TARGET_RATIO;
|
|
112
|
+
const maxEntries = Math.floor(targetSize / avgEntrySize);
|
|
113
|
+
const remaining = Object.entries(cache);
|
|
114
|
+
if (remaining.length > maxEntries) {
|
|
115
|
+
remaining.sort((a, b) => a[1].exp - b[1].exp);
|
|
116
|
+
const toDrop = remaining.length - maxEntries;
|
|
117
|
+
for (let i = 0; i < toDrop; i++) delete cache[remaining[i][0]];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Background purge triggered by setInterval.
|
|
122
|
+
* Checks the last purge timestamp to avoid redundant work across tabs,
|
|
123
|
+
* then removes expired entries. Only writes back if something changed.
|
|
124
|
+
* Timestamp is updated after the purge completes.
|
|
125
|
+
*/
|
|
126
|
+
_backgroundPurge() {
|
|
127
|
+
try {
|
|
128
|
+
const raw = localStorage.getItem(this._purgeTimestampKey);
|
|
129
|
+
const lastPurge = raw ? parseInt(raw, 10) : 0;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (now - lastPurge < this._purgeInterval) return;
|
|
132
|
+
const cache = this._readFromStorage();
|
|
133
|
+
const keysBefore = Object.keys(cache).length;
|
|
134
|
+
deleteExpiredEntries(cache, now);
|
|
135
|
+
if (Object.keys(cache).length < keysBefore) this._writeRaw(JSON.stringify(cache));
|
|
136
|
+
localStorage.setItem(this._purgeTimestampKey, String(now));
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Read and parse translations from localStorage.
|
|
141
|
+
* Recalibrates estimated size as a side effect.
|
|
142
|
+
* Returns empty object on any error (unavailable, corrupt data, etc.)
|
|
143
|
+
*/
|
|
144
|
+
_readFromStorage() {
|
|
145
|
+
try {
|
|
146
|
+
const raw = localStorage.getItem(this._storageKey);
|
|
147
|
+
if (!raw) {
|
|
148
|
+
this._estimatedSize = 0;
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
this._estimatedSize = raw.length;
|
|
152
|
+
return JSON.parse(raw);
|
|
153
|
+
} catch {
|
|
154
|
+
this._estimatedSize = 0;
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Persist new entries to localStorage with expiry timestamps.
|
|
160
|
+
* Reads current cache, merges buffer on top, writes back.
|
|
161
|
+
*/
|
|
162
|
+
initStorage(buffer) {
|
|
163
|
+
try {
|
|
164
|
+
const cache = this._readFromStorage();
|
|
165
|
+
const exp = Date.now() + this._ttl;
|
|
166
|
+
for (const [key, value] of Object.entries(buffer)) cache[key] = {
|
|
167
|
+
t: value,
|
|
168
|
+
exp
|
|
169
|
+
};
|
|
170
|
+
this._writeRaw(JSON.stringify(cache));
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Write a pre-serialized string to localStorage and recalibrate estimate.
|
|
175
|
+
*/
|
|
176
|
+
_writeRaw(serialized) {
|
|
177
|
+
try {
|
|
178
|
+
localStorage.setItem(this._storageKey, serialized);
|
|
179
|
+
this._estimatedSize = serialized.length;
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Helper function deletes expired entries from a cache in place.
|
|
185
|
+
*/
|
|
186
|
+
function deleteExpiredEntries(cache, now = Date.now()) {
|
|
187
|
+
for (const key of Object.keys(cache)) if (cache[key].exp <= now) delete cache[key];
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
export { LocalStorageTranslationCache };
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=LocalStorageTranslationCache-B66Ni5IM.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalStorageTranslationCache-B66Ni5IM.mjs","names":[],"sources":["../src/i18n-cache/LocalStorageTranslationCache.ts"],"sourcesContent":["import { Translation } from 'gt-i18n/types';\n\n// TODO: Add purge key/locks to prevent concurrent purges across tabs\n// TODO: Add cache key/locks for non-atomic read-modify-write operations across tabs\n\n// ===== Types ===== //\n\n/** A cached translation entry with expiry metadata */\ntype CachedEntry = { t: Translation; exp: number };\n\n// ===== Constants ===== //\n\nconst STORAGE_KEY_PREFIX = 'gt:tx:';\nconst PURGE_TIMESTAMP_PREFIX = 'gt:tx:purge:';\nconst FLUSH_INTERVAL = 500;\nconst DEFAULT_MAX_SIZE = 1_000_000; // ~1M characters (localStorage uses UTF-16)\nconst DEFAULT_TTL_MS = 86_400_000; // 24 hours\nconst DEFAULT_PURGE_INTERVAL_MS = 300_000; // 5 minutes\nconst PURGE_TARGET_RATIO = 0.8; // purge down to 80% of max\n\n// Prevents interval leaks on HMR — keyed by storage key\nconst activeIntervals = new Map<string, ReturnType<typeof setInterval>>();\n\n// ===== Class ===== //\n\n/**\n * A localStorage-backed translation cache for a single locale.\n * Used in development mode only to persist runtime translations across page refreshes.\n *\n * Entries are stored with per-entry expiry timestamps and the cache is purged\n * when estimated size exceeds the configured maximum.\n */\nexport class LocalStorageTranslationCache {\n private _storageKey: string;\n private _writeBuffer: Record<string, Translation> = {};\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n private _estimatedSize: number = 0;\n private _maxSize: number;\n private _ttl: number;\n private _purgeInterval: number;\n private _purgeTimestampKey: string;\n\n /**\n * @param locale - The locale this cache is for\n * @param projectId - The project id (namespaces localStorage keys)\n * @param init - Optional initial translations to merge on top of localStorage data.\n * init values take priority over stale localStorage entries.\n * @param maxSize - Maximum cache size in characters (default: ~1M)\n * @param ttl - TTL in milliseconds for each entry (default: 24 hours)\n * @param purgeInterval - Background purge check interval in ms (default: 5 min)\n */\n constructor({\n locale,\n projectId,\n init,\n maxSize,\n ttl,\n purgeInterval,\n }: {\n locale: string;\n projectId: string;\n init?: Record<string, Translation>;\n maxSize?: number;\n ttl?: number;\n purgeInterval?: number;\n }) {\n this._storageKey = `${STORAGE_KEY_PREFIX}${projectId}:${locale}`;\n this._purgeTimestampKey = `${PURGE_TIMESTAMP_PREFIX}${projectId}:${locale}`;\n this._maxSize = maxSize ?? DEFAULT_MAX_SIZE;\n this._ttl = ttl ?? DEFAULT_TTL_MS;\n this._purgeInterval = purgeInterval ?? DEFAULT_PURGE_INTERVAL_MS;\n\n // Merge init values on top (init wins on conflict)\n if (init) {\n this.initStorage(init);\n }\n\n // Start background purge interval (clears any existing interval for HMR safety)\n if (activeIntervals.has(this._storageKey)) {\n clearInterval(activeIntervals.get(this._storageKey)!);\n }\n const intervalId = setInterval(\n () => this._backgroundPurge(),\n this._purgeInterval\n );\n activeIntervals.set(this._storageKey, intervalId);\n }\n\n /**\n * Returns the full translation map (cache + pending buffer writes).\n * Filters out expired entries. Buffer entries take priority.\n */\n getInternalCache(): Record<string, Translation> {\n const now = Date.now();\n const cache = this._readFromStorage();\n const result: Record<string, Translation> = {};\n\n for (const [key, entry] of Object.entries(cache)) {\n if (entry.exp > now) {\n result[key] = entry.t;\n }\n }\n\n // Buffer entries are always fresh\n Object.assign(result, this._writeBuffer);\n return result;\n }\n\n /**\n * Queue a translation for writing to localStorage.\n * Writes are batched via a debounced flush.\n */\n write(hash: string, translation: Translation): void {\n this._writeBuffer[hash] = translation;\n this._scheduleFlush();\n }\n\n /**\n * Remove specific entries from the cache by hash.\n */\n purge(hashes: string[]): void {\n const cache = this._readFromStorage();\n for (const hash of hashes) {\n delete cache[hash];\n }\n this._writeRaw(JSON.stringify(cache));\n }\n\n // ===== Private Methods ===== //\n\n /**\n * Schedule a flush of the write buffer.\n * Uses a leading throttle — the first write in a burst schedules a flush\n * after FLUSH_INTERVAL ms; subsequent writes before the timer fires are\n * batched into the same flush.\n */\n private _scheduleFlush(): void {\n if (this._flushTimer) return; // already scheduled\n this._flushTimer = setTimeout(() => {\n this._flushTimer = null;\n this._flush();\n }, FLUSH_INTERVAL);\n }\n\n /**\n * Merge the write buffer into the cache and persist to localStorage.\n * Purges before writing if estimated size exceeds max.\n */\n private _flush(): void {\n if (Object.keys(this._writeBuffer).length === 0) return;\n\n try {\n const cache = this._readFromStorage();\n const now = Date.now();\n\n // Purge if estimated size exceeds max\n if (this._estimatedSize > this._maxSize) {\n this._purgeCache(cache, now);\n }\n\n // Merge buffer entries with expiry\n const exp = now + this._ttl;\n for (const [key, value] of Object.entries(this._writeBuffer)) {\n cache[key] = { t: value, exp };\n }\n\n this._writeRaw(JSON.stringify(cache));\n } catch {\n // Silently fail\n }\n\n this._writeBuffer = {};\n }\n\n /**\n * Purge entries from the cache in place.\n * Phase 1: Remove expired entries.\n * Phase 2: If still over target, drop oldest entries by expiry time.\n */\n private _purgeCache(cache: Record<string, CachedEntry>, now: number): void {\n const keysBeforePurge = Object.keys(cache);\n if (keysBeforePurge.length === 0) return;\n\n const avgEntrySize = this._estimatedSize / keysBeforePurge.length;\n\n // Phase 1: Remove expired entries\n deleteExpiredEntries(cache, now);\n\n // Phase 2: If still over target, drop oldest entries\n const targetSize = this._maxSize * PURGE_TARGET_RATIO;\n const maxEntries = Math.floor(targetSize / avgEntrySize);\n\n const remaining = Object.entries(cache);\n if (remaining.length > maxEntries) {\n remaining.sort((a, b) => a[1].exp - b[1].exp); // oldest first\n const toDrop = remaining.length - maxEntries;\n for (let i = 0; i < toDrop; i++) {\n delete cache[remaining[i][0]];\n }\n }\n }\n\n /**\n * Background purge triggered by setInterval.\n * Checks the last purge timestamp to avoid redundant work across tabs,\n * then removes expired entries. Only writes back if something changed.\n * Timestamp is updated after the purge completes.\n */\n private _backgroundPurge(): void {\n try {\n // Check if a purge is needed (another tab may have purged recently)\n const raw = localStorage.getItem(this._purgeTimestampKey);\n const lastPurge = raw ? parseInt(raw, 10) : 0;\n const now = Date.now();\n\n if (now - lastPurge < this._purgeInterval) return;\n\n // Run TTL purge\n const cache = this._readFromStorage();\n const keysBefore = Object.keys(cache).length;\n\n deleteExpiredEntries(cache, now);\n\n // Only write back if something was actually purged\n if (Object.keys(cache).length < keysBefore) {\n this._writeRaw(JSON.stringify(cache));\n }\n\n // Update timestamp after purge completes\n localStorage.setItem(this._purgeTimestampKey, String(now));\n } catch {\n // Silently fail\n }\n }\n\n /**\n * Read and parse translations from localStorage.\n * Recalibrates estimated size as a side effect.\n * Returns empty object on any error (unavailable, corrupt data, etc.)\n */\n private _readFromStorage(): Record<string, CachedEntry> {\n try {\n const raw = localStorage.getItem(this._storageKey);\n if (!raw) {\n this._estimatedSize = 0;\n return {};\n }\n this._estimatedSize = raw.length;\n return JSON.parse(raw) as Record<string, CachedEntry>;\n } catch {\n this._estimatedSize = 0;\n return {};\n }\n }\n\n /**\n * Persist new entries to localStorage with expiry timestamps.\n * Reads current cache, merges buffer on top, writes back.\n */\n private initStorage(buffer: Record<string, Translation>): void {\n try {\n const cache = this._readFromStorage();\n const exp = Date.now() + this._ttl;\n\n for (const [key, value] of Object.entries(buffer)) {\n cache[key] = { t: value, exp };\n }\n\n this._writeRaw(JSON.stringify(cache));\n } catch {\n // Silently fail — localStorage may be unavailable or full\n }\n }\n\n /**\n * Write a pre-serialized string to localStorage and recalibrate estimate.\n */\n private _writeRaw(serialized: string): void {\n try {\n localStorage.setItem(this._storageKey, serialized);\n this._estimatedSize = serialized.length;\n } catch {\n // Silently fail — localStorage may be unavailable or full\n }\n }\n}\n\n// ===== Helper Functions ===== //\n\n/**\n * Helper function deletes expired entries from a cache in place.\n */\nfunction deleteExpiredEntries(\n cache: Record<string, CachedEntry>,\n now: number = Date.now()\n): void {\n for (const key of Object.keys(cache)) {\n if (cache[key].exp <= now) {\n delete cache[key];\n }\n }\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
//#region src/i18n-cache/LocalStorageTranslationCache.ts
|
|
2
|
+
const STORAGE_KEY_PREFIX = "gt:tx:";
|
|
3
|
+
const PURGE_TIMESTAMP_PREFIX = "gt:tx:purge:";
|
|
4
|
+
const FLUSH_INTERVAL = 500;
|
|
5
|
+
const DEFAULT_MAX_SIZE = 1e6;
|
|
6
|
+
const DEFAULT_TTL_MS = 864e5;
|
|
7
|
+
const DEFAULT_PURGE_INTERVAL_MS = 3e5;
|
|
8
|
+
const PURGE_TARGET_RATIO = .8;
|
|
9
|
+
const activeIntervals = /* @__PURE__ */ new Map();
|
|
10
|
+
/**
|
|
11
|
+
* A localStorage-backed translation cache for a single locale.
|
|
12
|
+
* Used in development mode only to persist runtime translations across page refreshes.
|
|
13
|
+
*
|
|
14
|
+
* Entries are stored with per-entry expiry timestamps and the cache is purged
|
|
15
|
+
* when estimated size exceeds the configured maximum.
|
|
16
|
+
*/
|
|
17
|
+
var LocalStorageTranslationCache = class {
|
|
18
|
+
/**
|
|
19
|
+
* @param locale - The locale this cache is for
|
|
20
|
+
* @param projectId - The project id (namespaces localStorage keys)
|
|
21
|
+
* @param init - Optional initial translations to merge on top of localStorage data.
|
|
22
|
+
* init values take priority over stale localStorage entries.
|
|
23
|
+
* @param maxSize - Maximum cache size in characters (default: ~1M)
|
|
24
|
+
* @param ttl - TTL in milliseconds for each entry (default: 24 hours)
|
|
25
|
+
* @param purgeInterval - Background purge check interval in ms (default: 5 min)
|
|
26
|
+
*/
|
|
27
|
+
constructor({ locale, projectId, init, maxSize, ttl, purgeInterval }) {
|
|
28
|
+
this._writeBuffer = {};
|
|
29
|
+
this._flushTimer = null;
|
|
30
|
+
this._estimatedSize = 0;
|
|
31
|
+
this._storageKey = `${STORAGE_KEY_PREFIX}${projectId}:${locale}`;
|
|
32
|
+
this._purgeTimestampKey = `${PURGE_TIMESTAMP_PREFIX}${projectId}:${locale}`;
|
|
33
|
+
this._maxSize = maxSize ?? DEFAULT_MAX_SIZE;
|
|
34
|
+
this._ttl = ttl ?? DEFAULT_TTL_MS;
|
|
35
|
+
this._purgeInterval = purgeInterval ?? DEFAULT_PURGE_INTERVAL_MS;
|
|
36
|
+
if (init) this.initStorage(init);
|
|
37
|
+
if (activeIntervals.has(this._storageKey)) clearInterval(activeIntervals.get(this._storageKey));
|
|
38
|
+
const intervalId = setInterval(() => this._backgroundPurge(), this._purgeInterval);
|
|
39
|
+
activeIntervals.set(this._storageKey, intervalId);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Returns the full translation map (cache + pending buffer writes).
|
|
43
|
+
* Filters out expired entries. Buffer entries take priority.
|
|
44
|
+
*/
|
|
45
|
+
getInternalCache() {
|
|
46
|
+
const now = Date.now();
|
|
47
|
+
const cache = this._readFromStorage();
|
|
48
|
+
const result = {};
|
|
49
|
+
for (const [key, entry] of Object.entries(cache)) if (entry.exp > now) result[key] = entry.t;
|
|
50
|
+
Object.assign(result, this._writeBuffer);
|
|
51
|
+
return result;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Queue a translation for writing to localStorage.
|
|
55
|
+
* Writes are batched via a debounced flush.
|
|
56
|
+
*/
|
|
57
|
+
write(hash, translation) {
|
|
58
|
+
this._writeBuffer[hash] = translation;
|
|
59
|
+
this._scheduleFlush();
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Remove specific entries from the cache by hash.
|
|
63
|
+
*/
|
|
64
|
+
purge(hashes) {
|
|
65
|
+
const cache = this._readFromStorage();
|
|
66
|
+
for (const hash of hashes) delete cache[hash];
|
|
67
|
+
this._writeRaw(JSON.stringify(cache));
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Schedule a flush of the write buffer.
|
|
71
|
+
* Uses a leading throttle — the first write in a burst schedules a flush
|
|
72
|
+
* after FLUSH_INTERVAL ms; subsequent writes before the timer fires are
|
|
73
|
+
* batched into the same flush.
|
|
74
|
+
*/
|
|
75
|
+
_scheduleFlush() {
|
|
76
|
+
if (this._flushTimer) return;
|
|
77
|
+
this._flushTimer = setTimeout(() => {
|
|
78
|
+
this._flushTimer = null;
|
|
79
|
+
this._flush();
|
|
80
|
+
}, FLUSH_INTERVAL);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Merge the write buffer into the cache and persist to localStorage.
|
|
84
|
+
* Purges before writing if estimated size exceeds max.
|
|
85
|
+
*/
|
|
86
|
+
_flush() {
|
|
87
|
+
if (Object.keys(this._writeBuffer).length === 0) return;
|
|
88
|
+
try {
|
|
89
|
+
const cache = this._readFromStorage();
|
|
90
|
+
const now = Date.now();
|
|
91
|
+
if (this._estimatedSize > this._maxSize) this._purgeCache(cache, now);
|
|
92
|
+
const exp = now + this._ttl;
|
|
93
|
+
for (const [key, value] of Object.entries(this._writeBuffer)) cache[key] = {
|
|
94
|
+
t: value,
|
|
95
|
+
exp
|
|
96
|
+
};
|
|
97
|
+
this._writeRaw(JSON.stringify(cache));
|
|
98
|
+
} catch {}
|
|
99
|
+
this._writeBuffer = {};
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Purge entries from the cache in place.
|
|
103
|
+
* Phase 1: Remove expired entries.
|
|
104
|
+
* Phase 2: If still over target, drop oldest entries by expiry time.
|
|
105
|
+
*/
|
|
106
|
+
_purgeCache(cache, now) {
|
|
107
|
+
const keysBeforePurge = Object.keys(cache);
|
|
108
|
+
if (keysBeforePurge.length === 0) return;
|
|
109
|
+
const avgEntrySize = this._estimatedSize / keysBeforePurge.length;
|
|
110
|
+
deleteExpiredEntries(cache, now);
|
|
111
|
+
const targetSize = this._maxSize * PURGE_TARGET_RATIO;
|
|
112
|
+
const maxEntries = Math.floor(targetSize / avgEntrySize);
|
|
113
|
+
const remaining = Object.entries(cache);
|
|
114
|
+
if (remaining.length > maxEntries) {
|
|
115
|
+
remaining.sort((a, b) => a[1].exp - b[1].exp);
|
|
116
|
+
const toDrop = remaining.length - maxEntries;
|
|
117
|
+
for (let i = 0; i < toDrop; i++) delete cache[remaining[i][0]];
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Background purge triggered by setInterval.
|
|
122
|
+
* Checks the last purge timestamp to avoid redundant work across tabs,
|
|
123
|
+
* then removes expired entries. Only writes back if something changed.
|
|
124
|
+
* Timestamp is updated after the purge completes.
|
|
125
|
+
*/
|
|
126
|
+
_backgroundPurge() {
|
|
127
|
+
try {
|
|
128
|
+
const raw = localStorage.getItem(this._purgeTimestampKey);
|
|
129
|
+
const lastPurge = raw ? parseInt(raw, 10) : 0;
|
|
130
|
+
const now = Date.now();
|
|
131
|
+
if (now - lastPurge < this._purgeInterval) return;
|
|
132
|
+
const cache = this._readFromStorage();
|
|
133
|
+
const keysBefore = Object.keys(cache).length;
|
|
134
|
+
deleteExpiredEntries(cache, now);
|
|
135
|
+
if (Object.keys(cache).length < keysBefore) this._writeRaw(JSON.stringify(cache));
|
|
136
|
+
localStorage.setItem(this._purgeTimestampKey, String(now));
|
|
137
|
+
} catch {}
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Read and parse translations from localStorage.
|
|
141
|
+
* Recalibrates estimated size as a side effect.
|
|
142
|
+
* Returns empty object on any error (unavailable, corrupt data, etc.)
|
|
143
|
+
*/
|
|
144
|
+
_readFromStorage() {
|
|
145
|
+
try {
|
|
146
|
+
const raw = localStorage.getItem(this._storageKey);
|
|
147
|
+
if (!raw) {
|
|
148
|
+
this._estimatedSize = 0;
|
|
149
|
+
return {};
|
|
150
|
+
}
|
|
151
|
+
this._estimatedSize = raw.length;
|
|
152
|
+
return JSON.parse(raw);
|
|
153
|
+
} catch {
|
|
154
|
+
this._estimatedSize = 0;
|
|
155
|
+
return {};
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Persist new entries to localStorage with expiry timestamps.
|
|
160
|
+
* Reads current cache, merges buffer on top, writes back.
|
|
161
|
+
*/
|
|
162
|
+
initStorage(buffer) {
|
|
163
|
+
try {
|
|
164
|
+
const cache = this._readFromStorage();
|
|
165
|
+
const exp = Date.now() + this._ttl;
|
|
166
|
+
for (const [key, value] of Object.entries(buffer)) cache[key] = {
|
|
167
|
+
t: value,
|
|
168
|
+
exp
|
|
169
|
+
};
|
|
170
|
+
this._writeRaw(JSON.stringify(cache));
|
|
171
|
+
} catch {}
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Write a pre-serialized string to localStorage and recalibrate estimate.
|
|
175
|
+
*/
|
|
176
|
+
_writeRaw(serialized) {
|
|
177
|
+
try {
|
|
178
|
+
localStorage.setItem(this._storageKey, serialized);
|
|
179
|
+
this._estimatedSize = serialized.length;
|
|
180
|
+
} catch {}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
/**
|
|
184
|
+
* Helper function deletes expired entries from a cache in place.
|
|
185
|
+
*/
|
|
186
|
+
function deleteExpiredEntries(cache, now = Date.now()) {
|
|
187
|
+
for (const key of Object.keys(cache)) if (cache[key].exp <= now) delete cache[key];
|
|
188
|
+
}
|
|
189
|
+
//#endregion
|
|
190
|
+
exports.LocalStorageTranslationCache = LocalStorageTranslationCache;
|
|
191
|
+
|
|
192
|
+
//# sourceMappingURL=LocalStorageTranslationCache-DOEJz82p.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"LocalStorageTranslationCache-DOEJz82p.cjs","names":[],"sources":["../src/i18n-cache/LocalStorageTranslationCache.ts"],"sourcesContent":["import { Translation } from 'gt-i18n/types';\n\n// TODO: Add purge key/locks to prevent concurrent purges across tabs\n// TODO: Add cache key/locks for non-atomic read-modify-write operations across tabs\n\n// ===== Types ===== //\n\n/** A cached translation entry with expiry metadata */\ntype CachedEntry = { t: Translation; exp: number };\n\n// ===== Constants ===== //\n\nconst STORAGE_KEY_PREFIX = 'gt:tx:';\nconst PURGE_TIMESTAMP_PREFIX = 'gt:tx:purge:';\nconst FLUSH_INTERVAL = 500;\nconst DEFAULT_MAX_SIZE = 1_000_000; // ~1M characters (localStorage uses UTF-16)\nconst DEFAULT_TTL_MS = 86_400_000; // 24 hours\nconst DEFAULT_PURGE_INTERVAL_MS = 300_000; // 5 minutes\nconst PURGE_TARGET_RATIO = 0.8; // purge down to 80% of max\n\n// Prevents interval leaks on HMR — keyed by storage key\nconst activeIntervals = new Map<string, ReturnType<typeof setInterval>>();\n\n// ===== Class ===== //\n\n/**\n * A localStorage-backed translation cache for a single locale.\n * Used in development mode only to persist runtime translations across page refreshes.\n *\n * Entries are stored with per-entry expiry timestamps and the cache is purged\n * when estimated size exceeds the configured maximum.\n */\nexport class LocalStorageTranslationCache {\n private _storageKey: string;\n private _writeBuffer: Record<string, Translation> = {};\n private _flushTimer: ReturnType<typeof setTimeout> | null = null;\n private _estimatedSize: number = 0;\n private _maxSize: number;\n private _ttl: number;\n private _purgeInterval: number;\n private _purgeTimestampKey: string;\n\n /**\n * @param locale - The locale this cache is for\n * @param projectId - The project id (namespaces localStorage keys)\n * @param init - Optional initial translations to merge on top of localStorage data.\n * init values take priority over stale localStorage entries.\n * @param maxSize - Maximum cache size in characters (default: ~1M)\n * @param ttl - TTL in milliseconds for each entry (default: 24 hours)\n * @param purgeInterval - Background purge check interval in ms (default: 5 min)\n */\n constructor({\n locale,\n projectId,\n init,\n maxSize,\n ttl,\n purgeInterval,\n }: {\n locale: string;\n projectId: string;\n init?: Record<string, Translation>;\n maxSize?: number;\n ttl?: number;\n purgeInterval?: number;\n }) {\n this._storageKey = `${STORAGE_KEY_PREFIX}${projectId}:${locale}`;\n this._purgeTimestampKey = `${PURGE_TIMESTAMP_PREFIX}${projectId}:${locale}`;\n this._maxSize = maxSize ?? DEFAULT_MAX_SIZE;\n this._ttl = ttl ?? DEFAULT_TTL_MS;\n this._purgeInterval = purgeInterval ?? DEFAULT_PURGE_INTERVAL_MS;\n\n // Merge init values on top (init wins on conflict)\n if (init) {\n this.initStorage(init);\n }\n\n // Start background purge interval (clears any existing interval for HMR safety)\n if (activeIntervals.has(this._storageKey)) {\n clearInterval(activeIntervals.get(this._storageKey)!);\n }\n const intervalId = setInterval(\n () => this._backgroundPurge(),\n this._purgeInterval\n );\n activeIntervals.set(this._storageKey, intervalId);\n }\n\n /**\n * Returns the full translation map (cache + pending buffer writes).\n * Filters out expired entries. Buffer entries take priority.\n */\n getInternalCache(): Record<string, Translation> {\n const now = Date.now();\n const cache = this._readFromStorage();\n const result: Record<string, Translation> = {};\n\n for (const [key, entry] of Object.entries(cache)) {\n if (entry.exp > now) {\n result[key] = entry.t;\n }\n }\n\n // Buffer entries are always fresh\n Object.assign(result, this._writeBuffer);\n return result;\n }\n\n /**\n * Queue a translation for writing to localStorage.\n * Writes are batched via a debounced flush.\n */\n write(hash: string, translation: Translation): void {\n this._writeBuffer[hash] = translation;\n this._scheduleFlush();\n }\n\n /**\n * Remove specific entries from the cache by hash.\n */\n purge(hashes: string[]): void {\n const cache = this._readFromStorage();\n for (const hash of hashes) {\n delete cache[hash];\n }\n this._writeRaw(JSON.stringify(cache));\n }\n\n // ===== Private Methods ===== //\n\n /**\n * Schedule a flush of the write buffer.\n * Uses a leading throttle — the first write in a burst schedules a flush\n * after FLUSH_INTERVAL ms; subsequent writes before the timer fires are\n * batched into the same flush.\n */\n private _scheduleFlush(): void {\n if (this._flushTimer) return; // already scheduled\n this._flushTimer = setTimeout(() => {\n this._flushTimer = null;\n this._flush();\n }, FLUSH_INTERVAL);\n }\n\n /**\n * Merge the write buffer into the cache and persist to localStorage.\n * Purges before writing if estimated size exceeds max.\n */\n private _flush(): void {\n if (Object.keys(this._writeBuffer).length === 0) return;\n\n try {\n const cache = this._readFromStorage();\n const now = Date.now();\n\n // Purge if estimated size exceeds max\n if (this._estimatedSize > this._maxSize) {\n this._purgeCache(cache, now);\n }\n\n // Merge buffer entries with expiry\n const exp = now + this._ttl;\n for (const [key, value] of Object.entries(this._writeBuffer)) {\n cache[key] = { t: value, exp };\n }\n\n this._writeRaw(JSON.stringify(cache));\n } catch {\n // Silently fail\n }\n\n this._writeBuffer = {};\n }\n\n /**\n * Purge entries from the cache in place.\n * Phase 1: Remove expired entries.\n * Phase 2: If still over target, drop oldest entries by expiry time.\n */\n private _purgeCache(cache: Record<string, CachedEntry>, now: number): void {\n const keysBeforePurge = Object.keys(cache);\n if (keysBeforePurge.length === 0) return;\n\n const avgEntrySize = this._estimatedSize / keysBeforePurge.length;\n\n // Phase 1: Remove expired entries\n deleteExpiredEntries(cache, now);\n\n // Phase 2: If still over target, drop oldest entries\n const targetSize = this._maxSize * PURGE_TARGET_RATIO;\n const maxEntries = Math.floor(targetSize / avgEntrySize);\n\n const remaining = Object.entries(cache);\n if (remaining.length > maxEntries) {\n remaining.sort((a, b) => a[1].exp - b[1].exp); // oldest first\n const toDrop = remaining.length - maxEntries;\n for (let i = 0; i < toDrop; i++) {\n delete cache[remaining[i][0]];\n }\n }\n }\n\n /**\n * Background purge triggered by setInterval.\n * Checks the last purge timestamp to avoid redundant work across tabs,\n * then removes expired entries. Only writes back if something changed.\n * Timestamp is updated after the purge completes.\n */\n private _backgroundPurge(): void {\n try {\n // Check if a purge is needed (another tab may have purged recently)\n const raw = localStorage.getItem(this._purgeTimestampKey);\n const lastPurge = raw ? parseInt(raw, 10) : 0;\n const now = Date.now();\n\n if (now - lastPurge < this._purgeInterval) return;\n\n // Run TTL purge\n const cache = this._readFromStorage();\n const keysBefore = Object.keys(cache).length;\n\n deleteExpiredEntries(cache, now);\n\n // Only write back if something was actually purged\n if (Object.keys(cache).length < keysBefore) {\n this._writeRaw(JSON.stringify(cache));\n }\n\n // Update timestamp after purge completes\n localStorage.setItem(this._purgeTimestampKey, String(now));\n } catch {\n // Silently fail\n }\n }\n\n /**\n * Read and parse translations from localStorage.\n * Recalibrates estimated size as a side effect.\n * Returns empty object on any error (unavailable, corrupt data, etc.)\n */\n private _readFromStorage(): Record<string, CachedEntry> {\n try {\n const raw = localStorage.getItem(this._storageKey);\n if (!raw) {\n this._estimatedSize = 0;\n return {};\n }\n this._estimatedSize = raw.length;\n return JSON.parse(raw) as Record<string, CachedEntry>;\n } catch {\n this._estimatedSize = 0;\n return {};\n }\n }\n\n /**\n * Persist new entries to localStorage with expiry timestamps.\n * Reads current cache, merges buffer on top, writes back.\n */\n private initStorage(buffer: Record<string, Translation>): void {\n try {\n const cache = this._readFromStorage();\n const exp = Date.now() + this._ttl;\n\n for (const [key, value] of Object.entries(buffer)) {\n cache[key] = { t: value, exp };\n }\n\n this._writeRaw(JSON.stringify(cache));\n } catch {\n // Silently fail — localStorage may be unavailable or full\n }\n }\n\n /**\n * Write a pre-serialized string to localStorage and recalibrate estimate.\n */\n private _writeRaw(serialized: string): void {\n try {\n localStorage.setItem(this._storageKey, serialized);\n this._estimatedSize = serialized.length;\n } catch {\n // Silently fail — localStorage may be unavailable or full\n }\n }\n}\n\n// ===== Helper Functions ===== //\n\n/**\n * Helper function deletes expired entries from a cache in place.\n */\nfunction deleteExpiredEntries(\n cache: Record<string, CachedEntry>,\n now: number = Date.now()\n): void {\n for (const key of Object.keys(cache)) {\n if (cache[key].exp <= now) {\n delete cache[key];\n }\n }\n}\n"],"mappings":"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"}
|