@form-engine-ts/translator-cache 2.7.0 → 2.9.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/README.md CHANGED
@@ -25,3 +25,7 @@ const translator = withTranslationCache(baseTranslator, cache, {
25
25
  Keys isolate adapter name, source locale, target locale, and a deterministic UTF-8 hash of the source text. Batch misses
26
26
  are deduplicated, translated once in source order, cached, and restored to their original positions. The built-in memory
27
27
  cache applies both TTL expiration and bounded LRU eviction and exposes `size`, `evictionCount`, and `clear()` diagnostics.
28
+ Use `variant` to isolate glossary/model/configuration revisions, or `buildKey` for complete key control.
29
+ `onStatsReport` receives cumulative real cache hits, misses, evictions, and current size after successful translations.
30
+ Cache backend failures default to `cacheErrorPolicy: "bypass"`: `onCacheError` is notified and translation continues
31
+ through the wrapped adapter. Use `cacheErrorPolicy: "throw"` when cache availability is mandatory.
package/dist/index.cjs CHANGED
@@ -107,14 +107,73 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
107
107
  if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
108
108
  const adapterName = options.adapterName ?? "anonymous";
109
109
  if (adapterName.trim().length === 0) throw new TypeError("adapterName must not be empty.");
110
+ if (options.variant !== void 0 && options.variant.trim().length === 0) {
111
+ throw new TypeError("variant must not be empty.");
112
+ }
113
+ if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
114
+ throw new TypeError("buildKey must be a function.");
115
+ }
116
+ if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
117
+ throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
118
+ }
119
+ const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
120
+ const initialEvictionCount = cache.evictionCount ?? 0;
121
+ let hits = 0;
122
+ let misses = 0;
123
+ const handleCacheError = (cause, operation) => {
124
+ const error = cause instanceof Error ? cause : new Error(String(cause));
125
+ try {
126
+ options.onCacheError?.(error, operation);
127
+ } catch {
128
+ }
129
+ return error;
130
+ };
131
+ const getCached = async (key) => {
132
+ try {
133
+ return await cache.get(key);
134
+ } catch (cause) {
135
+ const error = handleCacheError(cause, "get");
136
+ if (cacheErrorPolicy === "throw") throw error;
137
+ return void 0;
138
+ }
139
+ };
140
+ const setCached = async (key, value) => {
141
+ try {
142
+ await cache.set(key, value, options.ttlMs);
143
+ } catch (cause) {
144
+ const error = handleCacheError(cause, "set");
145
+ if (cacheErrorPolicy === "throw") throw error;
146
+ }
147
+ };
148
+ const reportStats = () => {
149
+ const size = cache.size ?? 0;
150
+ options.onStatsReport?.({
151
+ hits,
152
+ misses,
153
+ evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
154
+ size
155
+ });
156
+ };
110
157
  const translateBatch = async (texts, targetLocale, sourceLocale) => {
111
158
  if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
112
159
  throw new TypeError("texts must be an array of strings.");
113
160
  }
114
161
  const target = requireLocale(targetLocale, "targetLocale");
115
162
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
116
- const keys = texts.map((text) => `${prefix}:${adapterName}:${source}:${target}:${hashTranslationText(text)}`);
117
- const cached = await Promise.all(keys.map((key) => cache.get(key)));
163
+ const keys = texts.map((text) => {
164
+ const context = {
165
+ sourceText: text,
166
+ sourceLocale: source,
167
+ targetLocale: target,
168
+ ...options.variant === void 0 ? {} : { variant: options.variant }
169
+ };
170
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
171
+ if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
172
+ return key;
173
+ });
174
+ const cached = await Promise.all(keys.map(getCached));
175
+ hits += cached.filter((value) => value !== void 0).length;
176
+ misses += cached.filter((value) => value === void 0).length;
118
177
  const missingByKey = /* @__PURE__ */ new Map();
119
178
  for (let index = 0; index < texts.length; index += 1) {
120
179
  if (cached[index] !== void 0) continue;
@@ -139,15 +198,17 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
139
198
  missing.map(async ([key, value], translatedIndex) => {
140
199
  const translation = translated[translatedIndex];
141
200
  if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
142
- await cache.set(key, translation, options.ttlMs);
201
+ await setCached(key, translation);
143
202
  for (const index of value.indices) cached[index] = translation;
144
203
  })
145
204
  );
146
205
  }
147
- return cached.map((value) => {
206
+ const result = cached.map((value) => {
148
207
  if (value === void 0) throw new Error("Translation cache result is unavailable.");
149
208
  return value;
150
209
  });
210
+ reportStats();
211
+ return result;
151
212
  };
152
213
  return {
153
214
  async translateText(text, targetLocale, sourceLocale) {
package/dist/index.d.cts CHANGED
@@ -1,13 +1,32 @@
1
1
  import { AsyncTranslationAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface TranslationCacheStorage {
4
+ readonly size?: number;
5
+ readonly evictionCount?: number;
4
6
  get(key: string): Promise<string | undefined> | string | undefined;
5
7
  set(key: string, value: string, ttlMs?: number): Promise<void> | void;
6
8
  }
9
+ interface TranslationCacheKeyContext {
10
+ readonly sourceText: string;
11
+ readonly sourceLocale: string;
12
+ readonly targetLocale: string;
13
+ readonly variant?: string;
14
+ }
15
+ interface TranslationCacheStats {
16
+ readonly hits: number;
17
+ readonly misses: number;
18
+ readonly evictions: number;
19
+ readonly size: number;
20
+ }
7
21
  interface TranslationCacheOptions {
8
22
  readonly ttlMs?: number;
9
23
  readonly keyPrefix?: string;
10
24
  readonly adapterName?: string;
25
+ readonly variant?: string;
26
+ readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
+ readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ readonly cacheErrorPolicy?: "bypass" | "throw";
29
+ readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
11
30
  }
12
31
  interface MemoryTranslationCacheOptions {
13
32
  readonly maxEntries?: number;
@@ -23,4 +42,4 @@ declare function hashTranslationText(text: string): string;
23
42
  declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
24
43
  declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
25
44
 
26
- export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheOptions, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
45
+ export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,32 @@
1
1
  import { AsyncTranslationAdapter } from '@form-engine-ts/core';
2
2
 
3
3
  interface TranslationCacheStorage {
4
+ readonly size?: number;
5
+ readonly evictionCount?: number;
4
6
  get(key: string): Promise<string | undefined> | string | undefined;
5
7
  set(key: string, value: string, ttlMs?: number): Promise<void> | void;
6
8
  }
9
+ interface TranslationCacheKeyContext {
10
+ readonly sourceText: string;
11
+ readonly sourceLocale: string;
12
+ readonly targetLocale: string;
13
+ readonly variant?: string;
14
+ }
15
+ interface TranslationCacheStats {
16
+ readonly hits: number;
17
+ readonly misses: number;
18
+ readonly evictions: number;
19
+ readonly size: number;
20
+ }
7
21
  interface TranslationCacheOptions {
8
22
  readonly ttlMs?: number;
9
23
  readonly keyPrefix?: string;
10
24
  readonly adapterName?: string;
25
+ readonly variant?: string;
26
+ readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
+ readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ readonly cacheErrorPolicy?: "bypass" | "throw";
29
+ readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
11
30
  }
12
31
  interface MemoryTranslationCacheOptions {
13
32
  readonly maxEntries?: number;
@@ -23,4 +42,4 @@ declare function hashTranslationText(text: string): string;
23
42
  declare function createMemoryTranslationCache(options?: MemoryTranslationCacheOptions): MemoryTranslationCache;
24
43
  declare function withTranslationCache(baseAdapter: AsyncTranslationAdapter, cache: TranslationCacheStorage, options?: TranslationCacheOptions): AsyncTranslationAdapter;
25
44
 
26
- export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheOptions, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
45
+ export { type MemoryTranslationCache, type MemoryTranslationCacheOptions, type TranslationCacheKeyContext, type TranslationCacheOptions, type TranslationCacheStats, type TranslationCacheStorage, createMemoryTranslationCache, hashTranslationText, withTranslationCache };
package/dist/index.js CHANGED
@@ -81,14 +81,73 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
81
81
  if (prefix.trim().length === 0) throw new TypeError("keyPrefix must not be empty.");
82
82
  const adapterName = options.adapterName ?? "anonymous";
83
83
  if (adapterName.trim().length === 0) throw new TypeError("adapterName must not be empty.");
84
+ if (options.variant !== void 0 && options.variant.trim().length === 0) {
85
+ throw new TypeError("variant must not be empty.");
86
+ }
87
+ if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
88
+ throw new TypeError("buildKey must be a function.");
89
+ }
90
+ if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
91
+ throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
92
+ }
93
+ const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
94
+ const initialEvictionCount = cache.evictionCount ?? 0;
95
+ let hits = 0;
96
+ let misses = 0;
97
+ const handleCacheError = (cause, operation) => {
98
+ const error = cause instanceof Error ? cause : new Error(String(cause));
99
+ try {
100
+ options.onCacheError?.(error, operation);
101
+ } catch {
102
+ }
103
+ return error;
104
+ };
105
+ const getCached = async (key) => {
106
+ try {
107
+ return await cache.get(key);
108
+ } catch (cause) {
109
+ const error = handleCacheError(cause, "get");
110
+ if (cacheErrorPolicy === "throw") throw error;
111
+ return void 0;
112
+ }
113
+ };
114
+ const setCached = async (key, value) => {
115
+ try {
116
+ await cache.set(key, value, options.ttlMs);
117
+ } catch (cause) {
118
+ const error = handleCacheError(cause, "set");
119
+ if (cacheErrorPolicy === "throw") throw error;
120
+ }
121
+ };
122
+ const reportStats = () => {
123
+ const size = cache.size ?? 0;
124
+ options.onStatsReport?.({
125
+ hits,
126
+ misses,
127
+ evictions: Math.max(0, (cache.evictionCount ?? initialEvictionCount) - initialEvictionCount),
128
+ size
129
+ });
130
+ };
84
131
  const translateBatch = async (texts, targetLocale, sourceLocale) => {
85
132
  if (!Array.isArray(texts) || texts.some((text) => typeof text !== "string")) {
86
133
  throw new TypeError("texts must be an array of strings.");
87
134
  }
88
135
  const target = requireLocale(targetLocale, "targetLocale");
89
136
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
90
- const keys = texts.map((text) => `${prefix}:${adapterName}:${source}:${target}:${hashTranslationText(text)}`);
91
- const cached = await Promise.all(keys.map((key) => cache.get(key)));
137
+ const keys = texts.map((text) => {
138
+ const context = {
139
+ sourceText: text,
140
+ sourceLocale: source,
141
+ targetLocale: target,
142
+ ...options.variant === void 0 ? {} : { variant: options.variant }
143
+ };
144
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
145
+ if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
146
+ return key;
147
+ });
148
+ const cached = await Promise.all(keys.map(getCached));
149
+ hits += cached.filter((value) => value !== void 0).length;
150
+ misses += cached.filter((value) => value === void 0).length;
92
151
  const missingByKey = /* @__PURE__ */ new Map();
93
152
  for (let index = 0; index < texts.length; index += 1) {
94
153
  if (cached[index] !== void 0) continue;
@@ -113,15 +172,17 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
113
172
  missing.map(async ([key, value], translatedIndex) => {
114
173
  const translation = translated[translatedIndex];
115
174
  if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
116
- await cache.set(key, translation, options.ttlMs);
175
+ await setCached(key, translation);
117
176
  for (const index of value.indices) cached[index] = translation;
118
177
  })
119
178
  );
120
179
  }
121
- return cached.map((value) => {
180
+ const result = cached.map((value) => {
122
181
  if (value === void 0) throw new Error("Translation cache result is unavailable.");
123
182
  return value;
124
183
  });
184
+ reportStats();
185
+ return result;
125
186
  };
126
187
  return {
127
188
  async translateText(text, targetLocale, sourceLocale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/translator-cache",
3
- "version": "2.7.0",
3
+ "version": "2.9.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -38,7 +38,7 @@
38
38
  "typescript"
39
39
  ],
40
40
  "dependencies": {
41
- "@form-engine-ts/core": "2.7.0"
41
+ "@form-engine-ts/core": "2.9.0"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",