@form-engine-ts/translator-cache 2.8.0 → 2.9.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/README.md CHANGED
@@ -26,4 +26,8 @@ Keys isolate adapter name, source locale, target locale, and a deterministic UTF
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
28
  Use `variant` to isolate glossary/model/configuration revisions, or `buildKey` for complete key control.
29
+ Adapters that expose a locale-aware cache variant (such as `@form-engine-ts/translator-google-v3` with a glossary)
30
+ are isolated automatically; an explicit `variant` is combined with that adapter variant.
29
31
  `onStatsReport` receives cumulative real cache hits, misses, evictions, and current size after successful translations.
32
+ Cache backend failures default to `cacheErrorPolicy: "bypass"`: `onCacheError` is notified and translation continues
33
+ through the wrapped adapter. Use `cacheErrorPolicy: "throw"` when cache availability is mandatory.
package/dist/index.cjs CHANGED
@@ -48,6 +48,11 @@ function nonNegativeDuration(value, fallback, name) {
48
48
  }
49
49
  return resolved;
50
50
  }
51
+ function getVariantProvider(adapter) {
52
+ if (typeof adapter !== "object" || adapter === null) return void 0;
53
+ const candidate = adapter;
54
+ return typeof candidate.getCacheVariant === "function" ? { getCacheVariant: candidate.getCacheVariant.bind(adapter) } : void 0;
55
+ }
51
56
  function createMemoryTranslationCache(options = {}) {
52
57
  const maxEntries = options.maxEntries ?? 500;
53
58
  if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
@@ -113,9 +118,39 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
113
118
  if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
114
119
  throw new TypeError("buildKey must be a function.");
115
120
  }
121
+ if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
122
+ throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
123
+ }
124
+ const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
116
125
  const initialEvictionCount = cache.evictionCount ?? 0;
117
126
  let hits = 0;
118
127
  let misses = 0;
128
+ const variantProvider = getVariantProvider(baseAdapter);
129
+ const handleCacheError = (cause, operation) => {
130
+ const error = cause instanceof Error ? cause : new Error(String(cause));
131
+ try {
132
+ options.onCacheError?.(error, operation);
133
+ } catch {
134
+ }
135
+ return error;
136
+ };
137
+ const getCached = async (key) => {
138
+ try {
139
+ return await cache.get(key);
140
+ } catch (cause) {
141
+ const error = handleCacheError(cause, "get");
142
+ if (cacheErrorPolicy === "throw") throw error;
143
+ return void 0;
144
+ }
145
+ };
146
+ const setCached = async (key, value) => {
147
+ try {
148
+ await cache.set(key, value, options.ttlMs);
149
+ } catch (cause) {
150
+ const error = handleCacheError(cause, "set");
151
+ if (cacheErrorPolicy === "throw") throw error;
152
+ }
153
+ };
119
154
  const reportStats = () => {
120
155
  const size = cache.size ?? 0;
121
156
  options.onStatsReport?.({
@@ -131,18 +166,23 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
131
166
  }
132
167
  const target = requireLocale(targetLocale, "targetLocale");
133
168
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
169
+ const automaticVariant = variantProvider?.getCacheVariant(target, sourceLocale);
170
+ if (automaticVariant !== void 0 && automaticVariant.trim().length === 0) {
171
+ throw new TypeError("baseAdapter cache variant must not be empty.");
172
+ }
173
+ const variant = options.variant === void 0 ? automaticVariant : automaticVariant === void 0 ? options.variant : `${options.variant}:${automaticVariant}`;
134
174
  const keys = texts.map((text) => {
135
175
  const context = {
136
176
  sourceText: text,
137
177
  sourceLocale: source,
138
178
  targetLocale: target,
139
- ...options.variant === void 0 ? {} : { variant: options.variant }
179
+ ...variant === void 0 ? {} : { variant }
140
180
  };
141
- const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
181
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${variant === void 0 ? "" : `${variant}:`}${source}:${target}:${hashTranslationText(text)}`;
142
182
  if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
143
183
  return key;
144
184
  });
145
- const cached = await Promise.all(keys.map((key) => cache.get(key)));
185
+ const cached = await Promise.all(keys.map(getCached));
146
186
  hits += cached.filter((value) => value !== void 0).length;
147
187
  misses += cached.filter((value) => value === void 0).length;
148
188
  const missingByKey = /* @__PURE__ */ new Map();
@@ -169,7 +209,7 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
169
209
  missing.map(async ([key, value], translatedIndex) => {
170
210
  const translation = translated[translatedIndex];
171
211
  if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
172
- await cache.set(key, translation, options.ttlMs);
212
+ await setCached(key, translation);
173
213
  for (const index of value.indices) cached[index] = translation;
174
214
  })
175
215
  );
package/dist/index.d.cts CHANGED
@@ -25,6 +25,8 @@ interface TranslationCacheOptions {
25
25
  readonly variant?: string;
26
26
  readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
27
  readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ readonly cacheErrorPolicy?: "bypass" | "throw";
29
+ readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
28
30
  }
29
31
  interface MemoryTranslationCacheOptions {
30
32
  readonly maxEntries?: number;
package/dist/index.d.ts CHANGED
@@ -25,6 +25,8 @@ interface TranslationCacheOptions {
25
25
  readonly variant?: string;
26
26
  readonly buildKey?: (context: TranslationCacheKeyContext) => string;
27
27
  readonly onStatsReport?: (stats: TranslationCacheStats) => void;
28
+ readonly cacheErrorPolicy?: "bypass" | "throw";
29
+ readonly onCacheError?: (error: Error, operation: "get" | "set") => void;
28
30
  }
29
31
  interface MemoryTranslationCacheOptions {
30
32
  readonly maxEntries?: number;
package/dist/index.js CHANGED
@@ -22,6 +22,11 @@ function nonNegativeDuration(value, fallback, name) {
22
22
  }
23
23
  return resolved;
24
24
  }
25
+ function getVariantProvider(adapter) {
26
+ if (typeof adapter !== "object" || adapter === null) return void 0;
27
+ const candidate = adapter;
28
+ return typeof candidate.getCacheVariant === "function" ? { getCacheVariant: candidate.getCacheVariant.bind(adapter) } : void 0;
29
+ }
25
30
  function createMemoryTranslationCache(options = {}) {
26
31
  const maxEntries = options.maxEntries ?? 500;
27
32
  if (!Number.isSafeInteger(maxEntries) || maxEntries < 1) {
@@ -87,9 +92,39 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
87
92
  if (options.buildKey !== void 0 && typeof options.buildKey !== "function") {
88
93
  throw new TypeError("buildKey must be a function.");
89
94
  }
95
+ if (options.cacheErrorPolicy !== void 0 && options.cacheErrorPolicy !== "bypass" && options.cacheErrorPolicy !== "throw") {
96
+ throw new TypeError('cacheErrorPolicy must be "bypass" or "throw".');
97
+ }
98
+ const cacheErrorPolicy = options.cacheErrorPolicy ?? "bypass";
90
99
  const initialEvictionCount = cache.evictionCount ?? 0;
91
100
  let hits = 0;
92
101
  let misses = 0;
102
+ const variantProvider = getVariantProvider(baseAdapter);
103
+ const handleCacheError = (cause, operation) => {
104
+ const error = cause instanceof Error ? cause : new Error(String(cause));
105
+ try {
106
+ options.onCacheError?.(error, operation);
107
+ } catch {
108
+ }
109
+ return error;
110
+ };
111
+ const getCached = async (key) => {
112
+ try {
113
+ return await cache.get(key);
114
+ } catch (cause) {
115
+ const error = handleCacheError(cause, "get");
116
+ if (cacheErrorPolicy === "throw") throw error;
117
+ return void 0;
118
+ }
119
+ };
120
+ const setCached = async (key, value) => {
121
+ try {
122
+ await cache.set(key, value, options.ttlMs);
123
+ } catch (cause) {
124
+ const error = handleCacheError(cause, "set");
125
+ if (cacheErrorPolicy === "throw") throw error;
126
+ }
127
+ };
93
128
  const reportStats = () => {
94
129
  const size = cache.size ?? 0;
95
130
  options.onStatsReport?.({
@@ -105,18 +140,23 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
105
140
  }
106
141
  const target = requireLocale(targetLocale, "targetLocale");
107
142
  const source = sourceLocale === void 0 ? "auto" : requireLocale(sourceLocale, "sourceLocale");
143
+ const automaticVariant = variantProvider?.getCacheVariant(target, sourceLocale);
144
+ if (automaticVariant !== void 0 && automaticVariant.trim().length === 0) {
145
+ throw new TypeError("baseAdapter cache variant must not be empty.");
146
+ }
147
+ const variant = options.variant === void 0 ? automaticVariant : automaticVariant === void 0 ? options.variant : `${options.variant}:${automaticVariant}`;
108
148
  const keys = texts.map((text) => {
109
149
  const context = {
110
150
  sourceText: text,
111
151
  sourceLocale: source,
112
152
  targetLocale: target,
113
- ...options.variant === void 0 ? {} : { variant: options.variant }
153
+ ...variant === void 0 ? {} : { variant }
114
154
  };
115
- const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${options.variant === void 0 ? "" : `${options.variant}:`}${source}:${target}:${hashTranslationText(text)}`;
155
+ const key = options.buildKey?.(context) ?? `${prefix}:${adapterName}:${variant === void 0 ? "" : `${variant}:`}${source}:${target}:${hashTranslationText(text)}`;
116
156
  if (typeof key !== "string" || key.length === 0) throw new TypeError("Translation cache key must not be empty.");
117
157
  return key;
118
158
  });
119
- const cached = await Promise.all(keys.map((key) => cache.get(key)));
159
+ const cached = await Promise.all(keys.map(getCached));
120
160
  hits += cached.filter((value) => value !== void 0).length;
121
161
  misses += cached.filter((value) => value === void 0).length;
122
162
  const missingByKey = /* @__PURE__ */ new Map();
@@ -143,7 +183,7 @@ function withTranslationCache(baseAdapter, cache, options = {}) {
143
183
  missing.map(async ([key, value], translatedIndex) => {
144
184
  const translation = translated[translatedIndex];
145
185
  if (translation === void 0) throw new Error("Translation adapter result is unavailable.");
146
- await cache.set(key, translation, options.ttlMs);
186
+ await setCached(key, translation);
147
187
  for (const index of value.indices) cached[index] = translation;
148
188
  })
149
189
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@form-engine-ts/translator-cache",
3
- "version": "2.8.0",
3
+ "version": "2.9.1",
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.8.0"
41
+ "@form-engine-ts/core": "2.9.1"
42
42
  },
43
43
  "scripts": {
44
44
  "build": "tsup src/index.ts --format esm,cjs --dts --clean --external @form-engine-ts/core",