transduck 0.7.0 → 0.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/dist/cli.js +1 -1
- package/dist/config.d.ts +1 -0
- package/dist/config.js +5 -0
- package/dist/handler.d.ts +9 -0
- package/dist/handler.js +70 -31
- package/dist/react/provider.d.ts +8 -3
- package/dist/react/provider.js +51 -22
- package/package.json +1 -1
- package/src/cli.ts +1 -1
- package/src/config.ts +9 -0
- package/src/handler.ts +162 -117
- package/src/react/provider.tsx +64 -22
- package/tests/ait.test.ts +1 -0
- package/tests/backend.test.ts +1 -0
- package/tests/config.test.ts +26 -0
- package/tests/handler.test.ts +170 -7
- package/tests/hooks.test.ts +1 -0
- package/tests/providers.test.ts +1 -0
- package/tests/react-provider.test.tsx +204 -22
package/src/handler.ts
CHANGED
|
@@ -10,6 +10,48 @@ function hash(text: string): string {
|
|
|
10
10
|
return createHash('sha256').update(text).digest('hex');
|
|
11
11
|
}
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Bounded-concurrency semaphore. Workers call `acquire()` to get a slot,
|
|
15
|
+
* then `release()` when done. When the active count is at the limit,
|
|
16
|
+
* further `acquire()` calls queue until a slot frees up.
|
|
17
|
+
*/
|
|
18
|
+
export function createSemaphore(limit: number) {
|
|
19
|
+
let active = 0;
|
|
20
|
+
const queue: Array<() => void> = [];
|
|
21
|
+
return {
|
|
22
|
+
acquire(): Promise<void> {
|
|
23
|
+
return new Promise(resolve => {
|
|
24
|
+
if (active < limit) {
|
|
25
|
+
active++;
|
|
26
|
+
resolve();
|
|
27
|
+
} else {
|
|
28
|
+
queue.push(() => {
|
|
29
|
+
active++;
|
|
30
|
+
resolve();
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
},
|
|
35
|
+
release(): void {
|
|
36
|
+
active--;
|
|
37
|
+
const next = queue.shift();
|
|
38
|
+
if (next) next();
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function withSlot<T>(
|
|
44
|
+
sem: ReturnType<typeof createSemaphore>,
|
|
45
|
+
fn: () => Promise<T>,
|
|
46
|
+
): Promise<T> {
|
|
47
|
+
await sem.acquire();
|
|
48
|
+
try {
|
|
49
|
+
return await fn();
|
|
50
|
+
} finally {
|
|
51
|
+
sem.release();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
13
55
|
interface TranslationRequestString {
|
|
14
56
|
text: string;
|
|
15
57
|
context?: string;
|
|
@@ -82,137 +124,140 @@ export async function handleTranslationRequest(
|
|
|
82
124
|
|
|
83
125
|
const projectContextHash = hash(cfg.projectContext);
|
|
84
126
|
|
|
85
|
-
|
|
86
|
-
|
|
127
|
+
// Single semaphore shared across strings and plurals so total outbound
|
|
128
|
+
// concurrency to the backend is bounded across both categories.
|
|
129
|
+
const sem = createSemaphore(cfg.backendMaxConcurrency);
|
|
87
130
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
// Tier 1: local cache
|
|
99
|
-
const cached = await store.lookup(lookupParams);
|
|
100
|
-
if (cached !== null) {
|
|
101
|
-
translations[key] = cached;
|
|
102
|
-
continue;
|
|
103
|
-
}
|
|
131
|
+
const stringWork = (body.strings ?? []).map(item =>
|
|
132
|
+
withSlot(sem, async (): Promise<[string, string]> => {
|
|
133
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
134
|
+
const stringContextHash = hash(item.context ?? '');
|
|
135
|
+
const key = `${item.text}||${item.context ?? ''}||${sourceLang}`;
|
|
136
|
+
const lookupParams = {
|
|
137
|
+
sourceText: item.text, sourceLang, targetLang,
|
|
138
|
+
projectContextHash, stringContextHash,
|
|
139
|
+
};
|
|
104
140
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
141
|
+
// Tier 1: local cache
|
|
142
|
+
const cached = await store.lookup(lookupParams);
|
|
143
|
+
if (cached !== null) return [key, cached];
|
|
144
|
+
|
|
145
|
+
// Tier 2: shared store
|
|
146
|
+
if (shared) {
|
|
147
|
+
const sharedCached = await shared.lookup(lookupParams);
|
|
148
|
+
if (sharedCached !== null) {
|
|
149
|
+
await store.insert({
|
|
150
|
+
...lookupParams, stringContext: item.context ?? '',
|
|
151
|
+
translatedText: sharedCached, model: cfg.backendModel, status: 'translated',
|
|
152
|
+
});
|
|
153
|
+
return [key, sharedCached];
|
|
154
|
+
}
|
|
116
155
|
}
|
|
117
|
-
}
|
|
118
156
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
};
|
|
131
|
-
await store.insert(insertParams);
|
|
132
|
-
if (shared) await shared.insert(insertParams);
|
|
133
|
-
if (opts?.onTranslate) {
|
|
134
|
-
const event: TranslationEvent = {
|
|
135
|
-
sourceText: item.text, translatedText: translated,
|
|
136
|
-
sourceLang, lang: targetLang, context: item.context ?? null,
|
|
137
|
-
projectContext: cfg.projectContext, source: 'translated',
|
|
138
|
-
plural: false,
|
|
157
|
+
// Backend call
|
|
158
|
+
try {
|
|
159
|
+
const translated = await backendTranslate(
|
|
160
|
+
item.text, sourceLang, targetLang,
|
|
161
|
+
cfg.projectContext, item.context ?? null, cfg,
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
if (validateTranslation(item.text, translated)) {
|
|
165
|
+
const insertParams = {
|
|
166
|
+
...lookupParams, stringContext: item.context ?? '',
|
|
167
|
+
translatedText: translated, model: cfg.backendModel, status: 'translated',
|
|
139
168
|
};
|
|
140
|
-
await
|
|
169
|
+
await store.insert(insertParams);
|
|
170
|
+
if (shared) await shared.insert(insertParams);
|
|
171
|
+
if (opts?.onTranslate) {
|
|
172
|
+
const event: TranslationEvent = {
|
|
173
|
+
sourceText: item.text, translatedText: translated,
|
|
174
|
+
sourceLang, lang: targetLang, context: item.context ?? null,
|
|
175
|
+
projectContext: cfg.projectContext, source: 'translated',
|
|
176
|
+
plural: false,
|
|
177
|
+
};
|
|
178
|
+
await fireHooks([], event, opts.onTranslate);
|
|
179
|
+
}
|
|
180
|
+
return [key, translated];
|
|
141
181
|
}
|
|
142
|
-
|
|
143
|
-
}
|
|
144
|
-
|
|
182
|
+
return [key, item.text];
|
|
183
|
+
} catch {
|
|
184
|
+
return [key, item.text];
|
|
145
185
|
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
}
|
|
149
|
-
}
|
|
186
|
+
})
|
|
187
|
+
);
|
|
150
188
|
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
189
|
+
const pluralWork = (body.plurals ?? []).map(item =>
|
|
190
|
+
withSlot(sem, async (): Promise<[string, Record<string, string>]> => {
|
|
191
|
+
const sourceLang = item.sourceLang?.toUpperCase() ?? bodySourceLang ?? cfg.sourceLang;
|
|
192
|
+
const stringContextHash = hash(item.context ?? '');
|
|
193
|
+
const sourceKey = item.one + '\x00' + item.other;
|
|
194
|
+
const responseKey = `${sourceKey}||${item.context ?? ''}||${sourceLang}`;
|
|
195
|
+
const lookupParams = {
|
|
196
|
+
sourceText: sourceKey, sourceLang, targetLang,
|
|
197
|
+
projectContextHash, stringContextHash,
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// Tier 1: local cache
|
|
201
|
+
const cachedForms = await store.lookupPlural(lookupParams);
|
|
202
|
+
if (Object.keys(cachedForms).length > 0) return [responseKey, cachedForms];
|
|
203
|
+
|
|
204
|
+
// Tier 2: shared store
|
|
205
|
+
if (shared) {
|
|
206
|
+
const sharedForms = await shared.lookupPlural(lookupParams);
|
|
207
|
+
if (Object.keys(sharedForms).length > 0) {
|
|
208
|
+
for (const [cat, text] of Object.entries(sharedForms)) {
|
|
209
|
+
await store.insertPlural({
|
|
210
|
+
...lookupParams, stringContext: item.context ?? '',
|
|
211
|
+
pluralCategory: cat, translatedText: text,
|
|
212
|
+
model: cfg.backendModel, status: 'translated',
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
return [responseKey, sharedForms];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Backend call
|
|
220
|
+
try {
|
|
221
|
+
const forms = await backendTranslatePlural(
|
|
222
|
+
item.one, item.other, sourceLang, targetLang,
|
|
223
|
+
cfg.projectContext, item.context ?? null, cfg,
|
|
224
|
+
);
|
|
168
225
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
const sharedForms = await shared.lookupPlural(lookupParams);
|
|
172
|
-
if (Object.keys(sharedForms).length > 0) {
|
|
173
|
-
// Propagate to local
|
|
174
|
-
for (const [cat, text] of Object.entries(sharedForms)) {
|
|
175
|
-
await store.insertPlural({
|
|
226
|
+
for (const [cat, translatedText] of Object.entries(forms)) {
|
|
227
|
+
const insertParams = {
|
|
176
228
|
...lookupParams, stringContext: item.context ?? '',
|
|
177
|
-
pluralCategory: cat, translatedText:
|
|
229
|
+
pluralCategory: cat, translatedText: translatedText as string,
|
|
178
230
|
model: cfg.backendModel, status: 'translated',
|
|
179
|
-
}
|
|
231
|
+
};
|
|
232
|
+
await store.insertPlural(insertParams);
|
|
233
|
+
if (shared) await shared.insertPlural(insertParams);
|
|
180
234
|
}
|
|
181
|
-
|
|
182
|
-
|
|
235
|
+
if (opts?.onTranslate && 'other' in forms) {
|
|
236
|
+
const event: TranslationEvent = {
|
|
237
|
+
sourceText: sourceKey, translatedText: forms.other ?? '',
|
|
238
|
+
sourceLang, lang: targetLang, context: item.context ?? null,
|
|
239
|
+
projectContext: cfg.projectContext, source: 'translated',
|
|
240
|
+
plural: true, pluralForms: forms,
|
|
241
|
+
};
|
|
242
|
+
await fireHooks([], event, opts.onTranslate);
|
|
243
|
+
}
|
|
244
|
+
return [responseKey, forms];
|
|
245
|
+
} catch {
|
|
246
|
+
return [responseKey, { one: item.one, other: item.other }];
|
|
183
247
|
}
|
|
184
|
-
}
|
|
248
|
+
})
|
|
249
|
+
);
|
|
185
250
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
pluralCategory: cat, translatedText: translatedText as string,
|
|
197
|
-
model: cfg.backendModel, status: 'translated',
|
|
198
|
-
};
|
|
199
|
-
await store.insertPlural(insertParams);
|
|
200
|
-
if (shared) await shared.insertPlural(insertParams);
|
|
201
|
-
}
|
|
202
|
-
if (opts?.onTranslate && 'other' in forms) {
|
|
203
|
-
const event: TranslationEvent = {
|
|
204
|
-
sourceText: sourceKey, translatedText: forms.other ?? '',
|
|
205
|
-
sourceLang, lang: targetLang, context: item.context ?? null,
|
|
206
|
-
projectContext: cfg.projectContext, source: 'translated',
|
|
207
|
-
plural: true, pluralForms: forms,
|
|
208
|
-
};
|
|
209
|
-
await fireHooks([], event, opts.onTranslate);
|
|
210
|
-
}
|
|
211
|
-
plurals[responseKey] = forms;
|
|
212
|
-
} catch {
|
|
213
|
-
plurals[responseKey] = { one: item.one, other: item.other };
|
|
214
|
-
}
|
|
215
|
-
}
|
|
251
|
+
const [stringResults, pluralResults] = await Promise.all([
|
|
252
|
+
Promise.all(stringWork),
|
|
253
|
+
Promise.all(pluralWork),
|
|
254
|
+
]);
|
|
255
|
+
|
|
256
|
+
const translations: Record<string, string> = {};
|
|
257
|
+
for (const [key, value] of stringResults) translations[key] = value;
|
|
258
|
+
|
|
259
|
+
const plurals: Record<string, Record<string, string>> = {};
|
|
260
|
+
for (const [key, value] of pluralResults) plurals[key] = value;
|
|
216
261
|
|
|
217
262
|
return { translations, plurals };
|
|
218
263
|
}
|
package/src/react/provider.tsx
CHANGED
|
@@ -130,19 +130,50 @@ function schedulePendingFlush() {
|
|
|
130
130
|
|
|
131
131
|
// --- Stable exported functions ---
|
|
132
132
|
|
|
133
|
+
export interface TOptions {
|
|
134
|
+
context?: string;
|
|
135
|
+
vars?: Record<string, string | number>;
|
|
136
|
+
/** Override the provider's source language for this call. */
|
|
137
|
+
sourceLang?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export interface TPluralOptions extends TOptions {}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Build the client-side cache / pending-set key.
|
|
144
|
+
* Format: text || context || sourceLang (last two always present)
|
|
145
|
+
*/
|
|
146
|
+
function buildKey(text: string, context: string | undefined, sourceLang: string): string {
|
|
147
|
+
return `${text}||${context ?? ''}||${sourceLang}`;
|
|
148
|
+
}
|
|
149
|
+
|
|
133
150
|
export function t(
|
|
134
151
|
sourceText: string,
|
|
135
|
-
|
|
152
|
+
contextOrOpts?: string | TOptions,
|
|
136
153
|
vars?: Record<string, string | number>,
|
|
137
154
|
): TranslationResult {
|
|
138
|
-
|
|
155
|
+
let context: string | undefined;
|
|
156
|
+
let resolvedVars: Record<string, string | number> | undefined;
|
|
157
|
+
let sourceLangOverride: string | undefined;
|
|
158
|
+
|
|
159
|
+
if (typeof contextOrOpts === 'string' || contextOrOpts === undefined) {
|
|
160
|
+
context = contextOrOpts;
|
|
161
|
+
resolvedVars = vars;
|
|
162
|
+
} else {
|
|
163
|
+
context = contextOrOpts.context;
|
|
164
|
+
resolvedVars = contextOrOpts.vars;
|
|
165
|
+
sourceLangOverride = contextOrOpts.sourceLang;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const effectiveSourceLang = (sourceLangOverride ?? _state.sourceLang).toUpperCase();
|
|
169
|
+
const key = buildKey(sourceText, context, effectiveSourceLang);
|
|
139
170
|
_state.knownKeys.add(key);
|
|
140
171
|
|
|
141
172
|
// Same language — return source
|
|
142
|
-
if (_state.language.toUpperCase() ===
|
|
143
|
-
return new TranslationResult(interpolateVars(sourceText,
|
|
173
|
+
if (_state.language.toUpperCase() === effectiveSourceLang) {
|
|
174
|
+
return new TranslationResult(interpolateVars(sourceText, resolvedVars), {
|
|
144
175
|
pending: false,
|
|
145
|
-
sourceLang:
|
|
176
|
+
sourceLang: effectiveSourceLang,
|
|
146
177
|
lang: _state.language,
|
|
147
178
|
});
|
|
148
179
|
}
|
|
@@ -150,9 +181,9 @@ export function t(
|
|
|
150
181
|
// Cache hit
|
|
151
182
|
const cached = _state.cache.get(key);
|
|
152
183
|
if (cached !== undefined) {
|
|
153
|
-
return new TranslationResult(interpolateVars(cached,
|
|
184
|
+
return new TranslationResult(interpolateVars(cached, resolvedVars), {
|
|
154
185
|
pending: false,
|
|
155
|
-
sourceLang:
|
|
186
|
+
sourceLang: effectiveSourceLang,
|
|
156
187
|
lang: _state.language,
|
|
157
188
|
source: 'cache',
|
|
158
189
|
});
|
|
@@ -163,9 +194,9 @@ export function t(
|
|
|
163
194
|
schedulePendingFlush();
|
|
164
195
|
|
|
165
196
|
// Return source text as fallback, marked pending
|
|
166
|
-
return new TranslationResult(interpolateVars(sourceText,
|
|
197
|
+
return new TranslationResult(interpolateVars(sourceText, resolvedVars), {
|
|
167
198
|
pending: true,
|
|
168
|
-
sourceLang:
|
|
199
|
+
sourceLang: effectiveSourceLang,
|
|
169
200
|
lang: _state.language,
|
|
170
201
|
});
|
|
171
202
|
}
|
|
@@ -174,9 +205,12 @@ export function tPlural(
|
|
|
174
205
|
one: string,
|
|
175
206
|
other: string,
|
|
176
207
|
count: number,
|
|
177
|
-
opts?:
|
|
208
|
+
opts?: TPluralOptions,
|
|
178
209
|
): TranslationResult {
|
|
179
210
|
const context = opts?.context;
|
|
211
|
+
const sourceLangOverride = opts?.sourceLang;
|
|
212
|
+
const effectiveSourceLang = (sourceLangOverride ?? _state.sourceLang).toUpperCase();
|
|
213
|
+
|
|
180
214
|
let vars: Record<string, string | number>;
|
|
181
215
|
if (!opts?.vars) {
|
|
182
216
|
vars = { count };
|
|
@@ -187,16 +221,16 @@ export function tPlural(
|
|
|
187
221
|
}
|
|
188
222
|
|
|
189
223
|
const sourceKey = `${one}\x00${other}`;
|
|
190
|
-
const cacheKey =
|
|
224
|
+
const cacheKey = buildKey(sourceKey, context, effectiveSourceLang);
|
|
191
225
|
_state.knownPlurals.add(cacheKey);
|
|
192
226
|
|
|
193
227
|
// Same language
|
|
194
|
-
if (_state.language.toUpperCase() ===
|
|
195
|
-
const rules = new Intl.PluralRules(
|
|
228
|
+
if (_state.language.toUpperCase() === effectiveSourceLang) {
|
|
229
|
+
const rules = new Intl.PluralRules(effectiveSourceLang.toLowerCase());
|
|
196
230
|
const form = rules.select(count) === 'one' ? one : other;
|
|
197
231
|
return new TranslationResult(interpolateVars(form, vars), {
|
|
198
232
|
pending: false,
|
|
199
|
-
sourceLang:
|
|
233
|
+
sourceLang: effectiveSourceLang,
|
|
200
234
|
lang: _state.language,
|
|
201
235
|
});
|
|
202
236
|
}
|
|
@@ -209,7 +243,7 @@ export function tPlural(
|
|
|
209
243
|
const form = cachedForms[category] ?? cachedForms['other'] ?? other;
|
|
210
244
|
return new TranslationResult(interpolateVars(form, vars), {
|
|
211
245
|
pending: false,
|
|
212
|
-
sourceLang:
|
|
246
|
+
sourceLang: effectiveSourceLang,
|
|
213
247
|
lang: _state.language,
|
|
214
248
|
source: 'cache',
|
|
215
249
|
});
|
|
@@ -220,11 +254,11 @@ export function tPlural(
|
|
|
220
254
|
schedulePendingFlush();
|
|
221
255
|
|
|
222
256
|
// Fallback to source form, marked pending
|
|
223
|
-
const rules = new Intl.PluralRules(
|
|
257
|
+
const rules = new Intl.PluralRules(effectiveSourceLang.toLowerCase());
|
|
224
258
|
const form = rules.select(count) === 'one' ? one : other;
|
|
225
259
|
return new TranslationResult(interpolateVars(form, vars), {
|
|
226
260
|
pending: true,
|
|
227
|
-
sourceLang:
|
|
261
|
+
sourceLang: effectiveSourceLang,
|
|
228
262
|
lang: _state.language,
|
|
229
263
|
});
|
|
230
264
|
}
|
|
@@ -365,15 +399,23 @@ export function TransDuckProvider({
|
|
|
365
399
|
return;
|
|
366
400
|
}
|
|
367
401
|
|
|
368
|
-
// Build request body
|
|
402
|
+
// Build request body.
|
|
403
|
+
// Key format: text || context || sourceLang. Pop the tail segments so text
|
|
404
|
+
// can contain "||" safely.
|
|
369
405
|
const strings = Array.from(stringsToFetch).map(key => {
|
|
370
|
-
const
|
|
371
|
-
|
|
406
|
+
const parts = key.split('||');
|
|
407
|
+
const sourceLang = parts.pop()!;
|
|
408
|
+
const context = parts.pop();
|
|
409
|
+
const text = parts.join('||');
|
|
410
|
+
return { text, context: context || undefined, sourceLang };
|
|
372
411
|
});
|
|
373
412
|
const plurals = Array.from(pluralsToFetch).map(key => {
|
|
374
|
-
const
|
|
413
|
+
const parts = key.split('||');
|
|
414
|
+
const sourceLang = parts.pop()!;
|
|
415
|
+
const context = parts.pop();
|
|
416
|
+
const sourceKey = parts.join('||');
|
|
375
417
|
const [one, other] = sourceKey.split('\x00');
|
|
376
|
-
return { one, other, context: context || undefined };
|
|
418
|
+
return { one, other, context: context || undefined, sourceLang };
|
|
377
419
|
});
|
|
378
420
|
|
|
379
421
|
try {
|
package/tests/ait.test.ts
CHANGED
package/tests/backend.test.ts
CHANGED
package/tests/config.test.ts
CHANGED
|
@@ -72,6 +72,32 @@ describe('loadConfig', () => {
|
|
|
72
72
|
delete process.env.TRANSDUCK_CONFIG;
|
|
73
73
|
});
|
|
74
74
|
|
|
75
|
+
it('defaults backendMaxConcurrency to 10 when absent', () => {
|
|
76
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
77
|
+
writeFileSync(configPath, VALID_YAML);
|
|
78
|
+
const cfg = loadConfig(configPath);
|
|
79
|
+
expect(cfg.backendMaxConcurrency).toBe(10);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('reads backendMaxConcurrency from backend.max_concurrency', () => {
|
|
83
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
84
|
+
writeFileSync(configPath, VALID_YAML + ' max_concurrency: 25\n');
|
|
85
|
+
const cfg = loadConfig(configPath);
|
|
86
|
+
expect(cfg.backendMaxConcurrency).toBe(25);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('rejects zero or negative backend.max_concurrency', () => {
|
|
90
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
91
|
+
writeFileSync(configPath, VALID_YAML + ' max_concurrency: 0\n');
|
|
92
|
+
expect(() => loadConfig(configPath)).toThrow(/max_concurrency/);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('rejects non-integer backend.max_concurrency', () => {
|
|
96
|
+
const configPath = join(tmpDir, 'transduck.yaml');
|
|
97
|
+
writeFileSync(configPath, VALID_YAML + ' max_concurrency: 2.5\n');
|
|
98
|
+
expect(() => loadConfig(configPath)).toThrow(/max_concurrency/);
|
|
99
|
+
});
|
|
100
|
+
|
|
75
101
|
it('defaults shared_url to null', () => {
|
|
76
102
|
const configPath = join(tmpDir, 'transduck.yaml');
|
|
77
103
|
writeFileSync(configPath, VALID_YAML);
|