Skip to content

Commit c815c50

Browse files
os-zhuangclaude
andauthored
perf(core): resolveLocalizationContext reads its three keys through settings.getMany (#10826) (#11208)
The caller half of #10826 (service half: #11200). One grouped namespace read replaces three per-key get()s — queries 16-18 of 24 on the measured rig collapse to one, with per-key answers unchanged by the service's equivalence contract. Feature-detected: an older service without getMany keeps the three parallel gets (still one leg — this is a query-count fix per the card's own calibration, not a latency fix). A thrown getMany lands exactly where a thrown get did: failed=true and the direct $in fallback, which reads the same three keys. Serial constraint honored: landed after #11197 (#10825) on this file. Co-authored-by: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
1 parent ee2ff45 commit c815c50

3 files changed

Lines changed: 93 additions & 13 deletions

File tree

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@objectstack/core': patch
3+
---
4+
5+
`resolveLocalizationContext` prefers `settings.getMany` — one grouped namespace read instead of three per-key `get()`s (#10826); older services without `getMany` keep the three parallel gets, and a thrown `getMany` lands in the same direct `$in` fallback a thrown `get` did.

packages/core/src/security/resolve-authz-context.test.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,60 @@ describe('resolveLocalizationContext — batched fallback read (#2409)', () => {
171171
expect(loc.locale).toBe('en-US');
172172
expect(loc.currency).toBeUndefined();
173173
});
174+
175+
// [#10826] The settings-service path prefers ONE grouped getMany over three
176+
// per-key get()s; an older service without getMany keeps the three gets;
177+
// a thrown getMany lands in the same direct-$in fallback a thrown get did.
178+
it('prefers settings.getMany (one grouped call) and never calls per-key get', async () => {
179+
const getMany = { calls: 0 };
180+
const settings = {
181+
get: async () => { throw new Error('per-key get must not be called'); },
182+
getMany: async (ns: string, keys: readonly string[]) => {
183+
getMany.calls += 1;
184+
expect(ns).toBe('localization');
185+
expect([...keys].sort()).toEqual(['currency', 'locale', 'timezone']);
186+
return {
187+
timezone: { value: 'Asia/Tokyo' },
188+
locale: { value: 'ja-JP' },
189+
currency: { value: 'JPY' },
190+
};
191+
},
192+
};
193+
const ql = makeCountingQl({ sys_setting: [] });
194+
const loc = await resolveLocalizationContext({ ql, settings, tenantId: 'o1' });
195+
expect(loc).toEqual({ timezone: 'Asia/Tokyo', locale: 'ja-JP', currency: 'JPY' });
196+
expect(getMany.calls).toBe(1);
197+
expect(ql.counts.sys_setting ?? 0).toBe(0); // service answered — no direct read
198+
});
199+
200+
it('a service without getMany keeps the three per-key gets (older deployments)', async () => {
201+
let gets = 0;
202+
const settings = {
203+
get: async (_ns: string, key: string) => {
204+
gets += 1;
205+
return { value: key === 'timezone' ? 'Asia/Tokyo' : key === 'locale' ? 'ja-JP' : 'JPY' };
206+
},
207+
};
208+
const ql = makeCountingQl({ sys_setting: [] });
209+
const loc = await resolveLocalizationContext({ ql, settings, tenantId: 'o1' });
210+
expect(loc).toEqual({ timezone: 'Asia/Tokyo', locale: 'ja-JP', currency: 'JPY' });
211+
expect(gets).toBe(3);
212+
});
213+
214+
it('a thrown getMany falls back to the direct $in read, same as a broken service', async () => {
215+
const settings = {
216+
get: async () => { throw new Error('unused'); },
217+
getMany: async () => { throw new Error('store exploded'); },
218+
};
219+
const ql = makeCountingQl({
220+
sys_setting: [
221+
{ namespace: 'localization', key: 'timezone', scope: 'tenant', value: 'Europe/Paris' },
222+
],
223+
});
224+
const loc = await resolveLocalizationContext({ ql, settings, tenantId: 'o1' });
225+
expect(loc.timezone).toBe('Europe/Paris');
226+
expect(ql.counts.sys_setting).toBe(1); // the batched $in fallback ran once
227+
});
174228
});
175229

176230
// #10221: a fresh environment's `sys_setting` table doesn't exist yet, so

packages/core/src/security/resolve-authz-context.ts

Lines changed: 34 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -748,20 +748,41 @@ async function resolveLocalizationContextUncached(
748748
try {
749749
if (settings && typeof settings.get === 'function') {
750750
const sctx = { tenantId, userId } as any;
751-
const [tzRes, localeRes, currencyRes] = await Promise.all([
752-
settings.get('localization', 'timezone', sctx).catch(() => {
751+
// [#10826] ONE grouped namespace read instead of three: `getMany`
752+
// resolves all three keys over at most two `loadRows` calls (queries
753+
// 16–18 of 24 on the measured rig collapse to one). Same per-key
754+
// answers by the service's own equivalence contract. Feature-detected:
755+
// an older service without `getMany` keeps the three parallel `get`s
756+
// (still 1 leg — this is a query-count fix, per the card's calibration).
757+
// A thrown `getMany` lands in the same place a thrown `get` did —
758+
// `failed = true` and the direct `$in` fallback below, which reads the
759+
// exact same three keys.
760+
let tzRes: any; let localeRes: any; let currencyRes: any;
761+
if (typeof settings.getMany === 'function') {
762+
try {
763+
const many = await settings.getMany('localization', ['timezone', 'locale', 'currency'], sctx);
764+
tzRes = many.timezone;
765+
localeRes = many.locale;
766+
currencyRes = many.currency;
767+
} catch {
753768
failed = true;
754-
return undefined;
755-
}),
756-
settings.get('localization', 'locale', sctx).catch(() => {
757-
failed = true;
758-
return undefined;
759-
}),
760-
settings.get('localization', 'currency', sctx).catch(() => {
761-
failed = true;
762-
return undefined;
763-
}),
764-
]);
769+
}
770+
} else {
771+
[tzRes, localeRes, currencyRes] = await Promise.all([
772+
settings.get('localization', 'timezone', sctx).catch(() => {
773+
failed = true;
774+
return undefined;
775+
}),
776+
settings.get('localization', 'locale', sctx).catch(() => {
777+
failed = true;
778+
return undefined;
779+
}),
780+
settings.get('localization', 'currency', sctx).catch(() => {
781+
failed = true;
782+
return undefined;
783+
}),
784+
]);
785+
}
765786
const tz = coerceTimeZone(tzRes?.value);
766787
const locale = coerceLocale(localeRes?.value);
767788
const currency = coerceCurrency(currencyRes?.value);

0 commit comments

Comments
 (0)