@open-mercato/core 0.6.4-develop.3921.1.8a42ddf4c8 → 0.6.4-develop.3929.1.fcf7afece2

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.
@@ -16,13 +16,19 @@ const getCacheTags = (identifier, tenantId) => {
16
16
  return [getIdentifierTag(identifier), getTenantTag(tenantId)];
17
17
  };
18
18
  class FeatureTogglesService {
19
- // 1 minute
20
19
  constructor(cache, em) {
21
20
  this.cache = cache;
22
21
  this.em = em;
23
22
  this.cacheTtlMs = 1 * 60 * 1e3;
23
+ // 1 minute
24
+ // Resolution cache can be disabled via env (e.g. integration tests that flip
25
+ // overrides rapidly between cases). The 1-minute TTL is a production
26
+ // optimization; under fast flag churn it can serve a stale value across
27
+ // override set/clear despite invalidation, so tests opt out for determinism.
28
+ this.cacheDisabled = process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "1" || process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === "true";
24
29
  }
25
30
  async saveCache(identifier, tenantId, result) {
31
+ if (this.cacheDisabled) return;
26
32
  const key = getIsEnabledCacheKey(identifier, tenantId);
27
33
  await runWithCacheTenant(
28
34
  tenantId,
@@ -31,10 +37,12 @@ class FeatureTogglesService {
31
37
  }
32
38
  async resolveToggle(identifier, tenantId) {
33
39
  const key = getIsEnabledCacheKey(identifier, tenantId);
34
- const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key));
35
- if (cached) {
36
- const parsed = toCachedResolution(cached);
37
- if (parsed) return parsed;
40
+ if (!this.cacheDisabled) {
41
+ const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key));
42
+ if (cached) {
43
+ const parsed = toCachedResolution(cached);
44
+ if (parsed) return parsed;
45
+ }
38
46
  }
39
47
  let toggle = null;
40
48
  toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../../../../src/modules/feature_toggles/lib/feature-flag-check.ts"],
4
- "sourcesContent": ["import { FeatureToggle, FeatureToggleOverride } from \"../data/entities\"\nimport { EntityManager } from \"@mikro-orm/core\"\nimport { CacheService, runWithCacheTenant } from \"@open-mercato/cache\"\n\ntype ToggleValueType = \"boolean\" | \"string\" | \"number\" | \"json\"\n\ntype ToggleResolutionSource = \"override\" | \"default\" | \"missing\"\n\ntype ToggleResolutionResult = {\n valueType: ToggleValueType\n value: boolean | string | number | unknown | null\n source: ToggleResolutionSource\n toggleId: string\n identifier: string\n tenantId: string\n}\n\ntype ToggleErrorCode = \"TYPE_MISMATCH\" | \"MISSING_TOGGLE\" | \"INVALID_VALUE\"\n\ntype ToggleError = {\n code: ToggleErrorCode\n message: string\n identifier: string\n expectedType: ToggleValueType\n actualType?: ToggleValueType\n source?: ToggleResolutionSource\n}\n\ntype ResultOk<T> = { ok: true; value: T; resolution: ToggleResolutionResult }\ntype ResultErr = { ok: false; error: ToggleError; resolution: ToggleResolutionResult }\nexport type Result<T> = ResultOk<T> | ResultErr\n\ntype ResolutionContext = {\n tenantId: string\n valueType: ToggleValueType\n}\n\nconst toCachedResolution = (value: unknown): ToggleResolutionResult | null => {\n if (typeof value !== \"object\" || value === null) return null\n const record = value as Partial<ToggleResolutionResult>\n if (\n !record.valueType ||\n typeof record.source !== \"string\" ||\n !record.toggleId ||\n !record.identifier ||\n !record.tenantId\n )\n return null\n return value as ToggleResolutionResult\n}\n\nexport const getIsEnabledCacheKey = (identifier: string, tenantId: string) => {\n return `feature_toggles:resolution:${identifier}:${tenantId}`\n}\n\nconst getIdentifierTag = (identifier: string) => `feature_toggles:identifier:${identifier}`\nconst getTenantTag = (tenantId: string) => `feature_toggles:tenant:${tenantId}`\n\nconst getCacheTags = (identifier: string, tenantId: string) => {\n return [getIdentifierTag(identifier), getTenantTag(tenantId)]\n}\n\nexport class FeatureTogglesService {\n private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute\n constructor(\n private readonly cache: CacheService,\n private readonly em: EntityManager\n ) { }\n\n private async saveCache(\n identifier: string,\n tenantId: string,\n result: ToggleResolutionResult,\n ) {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n await runWithCacheTenant(\n tenantId,\n () => this.cache.set(key, result, { ttl: this.cacheTtlMs, tags: getCacheTags(identifier, tenantId) }),\n )\n }\n\n private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n\n const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))\n if (cached) {\n const parsed = toCachedResolution(cached)\n if (parsed) return parsed\n }\n\n let toggle: FeatureToggle | null = null\n toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null })\n\n if (!toggle) {\n const result: ToggleResolutionResult = {\n valueType: \"boolean\",\n value: null,\n source: \"missing\",\n toggleId: \"\",\n identifier,\n tenantId,\n }\n return result\n }\n\n let override: FeatureToggleOverride | null = null\n override = await this.em.findOne(FeatureToggleOverride, { toggle: toggle.id, tenantId })\n\n\n const result: ToggleResolutionResult = {\n valueType: toggle.type,\n value: override ? override.value : toggle.defaultValue,\n source: override ? \"override\" : \"default\",\n toggleId: toggle.id,\n identifier: toggle.identifier,\n tenantId,\n }\n\n await this.saveCache(identifier, tenantId, result)\n return result\n }\n\n public async invalidateIsEnabledCacheByIdentifierTag(identifier: string) {\n await this.cache.deleteByTags([getIdentifierTag(identifier)])\n }\n\n public async invalidateIsEnabledCacheByKey(identifier: string, tenantId: string) {\n await runWithCacheTenant(tenantId, () => this.cache.delete(getIsEnabledCacheKey(identifier, tenantId)))\n }\n\n public async getFeatureToggleValue<T>(\n identifier: string,\n ctx: ResolutionContext\n ): Promise<Result<T>> {\n const resolution = await this.resolveToggle(identifier, ctx.tenantId)\n\n if (resolution.source === \"missing\") {\n console.warn(`[feature_toggles] Toggle \"${identifier}\" not found (missing).`)\n return {\n ok: false,\n error: {\n code: \"MISSING_TOGGLE\",\n message: `Toggle \"${identifier}\" not found (missing).`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n\n\n if (resolution.valueType !== ctx.valueType) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"TYPE_MISMATCH\",\n message: `Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n const isValueValid =\n (ctx.valueType === \"boolean\" && typeof resolution.value === \"boolean\") ||\n (ctx.valueType === \"string\" && typeof resolution.value === \"string\") ||\n (ctx.valueType === \"number\" && typeof resolution.value === \"number\") ||\n (ctx.valueType === \"json\")\n\n if (!isValueValid) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"INVALID_VALUE\",\n message: `Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n return {\n ok: true,\n value: resolution.value as T,\n resolution,\n }\n }\n\n public async getBoolConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<boolean>(identifier, { tenantId, valueType: \"boolean\" })\n }\n\n public async getNumberConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<number>(identifier, { tenantId, valueType: \"number\" })\n }\n\n public async getStringConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<string>(identifier, { tenantId, valueType: \"string\" })\n }\n\n public async getJsonConfig<T = unknown>(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<T>(identifier, { tenantId, valueType: \"json\" })\n }\n}\n"],
5
- "mappings": "AAAA,SAAS,eAAe,6BAA6B;AAErD,SAAuB,0BAA0B;AAmCjD,MAAM,qBAAqB,CAAC,UAAkD;AAC5E,MAAI,OAAO,UAAU,YAAY,UAAU,KAAM,QAAO;AACxD,QAAM,SAAS;AACf,MACE,CAAC,OAAO,aACR,OAAO,OAAO,WAAW,YACzB,CAAC,OAAO,YACR,CAAC,OAAO,cACR,CAAC,OAAO;AAER,WAAO;AACT,SAAO;AACT;AAEO,MAAM,uBAAuB,CAAC,YAAoB,aAAqB;AAC5E,SAAO,8BAA8B,UAAU,IAAI,QAAQ;AAC7D;AAEA,MAAM,mBAAmB,CAAC,eAAuB,8BAA8B,UAAU;AACzF,MAAM,eAAe,CAAC,aAAqB,0BAA0B,QAAQ;AAE7E,MAAM,eAAe,CAAC,YAAoB,aAAqB;AAC7D,SAAO,CAAC,iBAAiB,UAAU,GAAG,aAAa,QAAQ,CAAC;AAC9D;AAEO,MAAM,sBAAsB;AAAA;AAAA,EAEjC,YACmB,OACA,IACjB;AAFiB;AACA;AAHnB,SAAQ,aAAqB,IAAI,KAAK;AAAA,EAIlC;AAAA,EAEJ,MAAc,UACZ,YACA,UACA,QACA;AACA,UAAM,MAAM,qBAAqB,YAAY,QAAQ;AACrD,UAAM;AAAA,MACJ;AAAA,MACA,MAAM,KAAK,MAAM,IAAI,KAAK,QAAQ,EAAE,KAAK,KAAK,YAAY,MAAM,aAAa,YAAY,QAAQ,EAAE,CAAC;AAAA,IACtG;AAAA,EACF;AAAA,EAEA,MAAc,cAAc,YAAoB,UAAmD;AACjG,UAAM,MAAM,qBAAqB,YAAY,QAAQ;AAErD,UAAM,SAAS,MAAM,mBAAmB,UAAU,MAAM,KAAK,MAAM,IAAI,GAAG,CAAC;AAC3E,QAAI,QAAQ;AACV,YAAM,SAAS,mBAAmB,MAAM;AACxC,UAAI,OAAQ,QAAO;AAAA,IACrB;AAEA,QAAI,SAA+B;AACnC,aAAS,MAAM,KAAK,GAAG,QAAQ,eAAe,EAAE,YAAY,WAAW,KAAK,CAAC;AAE7E,QAAI,CAAC,QAAQ;AACX,YAAMA,UAAiC;AAAA,QACrC,WAAW;AAAA,QACX,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,UAAU;AAAA,QACV;AAAA,QACA;AAAA,MACF;AACA,aAAOA;AAAA,IACT;AAEA,QAAI,WAAyC;AAC7C,eAAW,MAAM,KAAK,GAAG,QAAQ,uBAAuB,EAAE,QAAQ,OAAO,IAAI,SAAS,CAAC;AAGvF,UAAM,SAAiC;AAAA,MACrC,WAAW,OAAO;AAAA,MAClB,OAAO,WAAW,SAAS,QAAQ,OAAO;AAAA,MAC1C,QAAQ,WAAW,aAAa;AAAA,MAChC,UAAU,OAAO;AAAA,MACjB,YAAY,OAAO;AAAA,MACnB;AAAA,IACF;AAEA,UAAM,KAAK,UAAU,YAAY,UAAU,MAAM;AACjD,WAAO;AAAA,EACT;AAAA,EAEA,MAAa,wCAAwC,YAAoB;AACvE,UAAM,KAAK,MAAM,aAAa,CAAC,iBAAiB,UAAU,CAAC,CAAC;AAAA,EAC9D;AAAA,EAEA,MAAa,8BAA8B,YAAoB,UAAkB;AAC/E,UAAM,mBAAmB,UAAU,MAAM,KAAK,MAAM,OAAO,qBAAqB,YAAY,QAAQ,CAAC,CAAC;AAAA,EACxG;AAAA,EAEA,MAAa,sBACX,YACA,KACoB;AACpB,UAAM,aAAa,MAAM,KAAK,cAAc,YAAY,IAAI,QAAQ;AAEpE,QAAI,WAAW,WAAW,WAAW;AACnC,cAAQ,KAAK,6BAA6B,UAAU,wBAAwB;AAC5E,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS,WAAW,UAAU;AAAA,UAC9B;AAAA,UACA,cAAc,IAAI;AAAA,UAClB,YAAY,WAAW;AAAA,UACvB,QAAQ,WAAW;AAAA,QACrB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAIA,QAAI,WAAW,cAAc,IAAI,WAAW;AAC1C,cAAQ;AAAA,QACN,6BAA6B,UAAU,eAAe,WAAW,SAAS,UAAU,IAAI,SAAS;AAAA,QACjG,EAAE,WAAW;AAAA,MACf;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS,WAAW,UAAU,eAAe,WAAW,SAAS,UAAU,IAAI,SAAS;AAAA,UACxF;AAAA,UACA,cAAc,IAAI;AAAA,UAClB,YAAY,WAAW;AAAA,UACvB,QAAQ,WAAW;AAAA,QACrB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,eACH,IAAI,cAAc,aAAa,OAAO,WAAW,UAAU,aAC3D,IAAI,cAAc,YAAY,OAAO,WAAW,UAAU,YAC1D,IAAI,cAAc,YAAY,OAAO,WAAW,UAAU,YAC1D,IAAI,cAAc;AAErB,QAAI,CAAC,cAAc;AACjB,cAAQ;AAAA,QACN,6BAA6B,UAAU,iCAAiC,WAAW,SAAS;AAAA,QAC5F,EAAE,WAAW;AAAA,MACf;AACA,aAAO;AAAA,QACL,IAAI;AAAA,QACJ,OAAO;AAAA,UACL,MAAM;AAAA,UACN,SAAS,WAAW,UAAU,iCAAiC,WAAW,SAAS;AAAA,UACnF;AAAA,UACA,cAAc,IAAI;AAAA,UAClB,YAAY,WAAW;AAAA,UACvB,QAAQ,WAAW;AAAA,QACrB;AAAA,QACA;AAAA,MACF;AAAA,IACF;AAEA,WAAO;AAAA,MACL,IAAI;AAAA,MACJ,OAAO,WAAW;AAAA,MAClB;AAAA,IACF;AAAA,EACF;AAAA,EAEA,MAAa,cAAc,YAAoB,UAAkB;AAC/D,WAAO,KAAK,sBAA+B,YAAY,EAAE,UAAU,WAAW,UAAU,CAAC;AAAA,EAC3F;AAAA,EAEA,MAAa,gBAAgB,YAAoB,UAAkB;AACjE,WAAO,KAAK,sBAA8B,YAAY,EAAE,UAAU,WAAW,SAAS,CAAC;AAAA,EACzF;AAAA,EAEA,MAAa,gBAAgB,YAAoB,UAAkB;AACjE,WAAO,KAAK,sBAA8B,YAAY,EAAE,UAAU,WAAW,SAAS,CAAC;AAAA,EACzF;AAAA,EAEA,MAAa,cAA2B,YAAoB,UAAkB;AAC5E,WAAO,KAAK,sBAAyB,YAAY,EAAE,UAAU,WAAW,OAAO,CAAC;AAAA,EAClF;AACF;",
4
+ "sourcesContent": ["import { FeatureToggle, FeatureToggleOverride } from \"../data/entities\"\nimport { EntityManager } from \"@mikro-orm/core\"\nimport { CacheService, runWithCacheTenant } from \"@open-mercato/cache\"\n\ntype ToggleValueType = \"boolean\" | \"string\" | \"number\" | \"json\"\n\ntype ToggleResolutionSource = \"override\" | \"default\" | \"missing\"\n\ntype ToggleResolutionResult = {\n valueType: ToggleValueType\n value: boolean | string | number | unknown | null\n source: ToggleResolutionSource\n toggleId: string\n identifier: string\n tenantId: string\n}\n\ntype ToggleErrorCode = \"TYPE_MISMATCH\" | \"MISSING_TOGGLE\" | \"INVALID_VALUE\"\n\ntype ToggleError = {\n code: ToggleErrorCode\n message: string\n identifier: string\n expectedType: ToggleValueType\n actualType?: ToggleValueType\n source?: ToggleResolutionSource\n}\n\ntype ResultOk<T> = { ok: true; value: T; resolution: ToggleResolutionResult }\ntype ResultErr = { ok: false; error: ToggleError; resolution: ToggleResolutionResult }\nexport type Result<T> = ResultOk<T> | ResultErr\n\ntype ResolutionContext = {\n tenantId: string\n valueType: ToggleValueType\n}\n\nconst toCachedResolution = (value: unknown): ToggleResolutionResult | null => {\n if (typeof value !== \"object\" || value === null) return null\n const record = value as Partial<ToggleResolutionResult>\n if (\n !record.valueType ||\n typeof record.source !== \"string\" ||\n !record.toggleId ||\n !record.identifier ||\n !record.tenantId\n )\n return null\n return value as ToggleResolutionResult\n}\n\nexport const getIsEnabledCacheKey = (identifier: string, tenantId: string) => {\n return `feature_toggles:resolution:${identifier}:${tenantId}`\n}\n\nconst getIdentifierTag = (identifier: string) => `feature_toggles:identifier:${identifier}`\nconst getTenantTag = (tenantId: string) => `feature_toggles:tenant:${tenantId}`\n\nconst getCacheTags = (identifier: string, tenantId: string) => {\n return [getIdentifierTag(identifier), getTenantTag(tenantId)]\n}\n\nexport class FeatureTogglesService {\n private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute\n // Resolution cache can be disabled via env (e.g. integration tests that flip\n // overrides rapidly between cases). The 1-minute TTL is a production\n // optimization; under fast flag churn it can serve a stale value across\n // override set/clear despite invalidation, so tests opt out for determinism.\n private readonly cacheDisabled: boolean =\n process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === '1' ||\n process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === 'true'\n constructor(\n private readonly cache: CacheService,\n private readonly em: EntityManager\n ) { }\n\n private async saveCache(\n identifier: string,\n tenantId: string,\n result: ToggleResolutionResult,\n ) {\n if (this.cacheDisabled) return\n const key = getIsEnabledCacheKey(identifier, tenantId)\n await runWithCacheTenant(\n tenantId,\n () => this.cache.set(key, result, { ttl: this.cacheTtlMs, tags: getCacheTags(identifier, tenantId) }),\n )\n }\n\n private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {\n const key = getIsEnabledCacheKey(identifier, tenantId)\n\n if (!this.cacheDisabled) {\n const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))\n if (cached) {\n const parsed = toCachedResolution(cached)\n if (parsed) return parsed\n }\n }\n\n let toggle: FeatureToggle | null = null\n toggle = await this.em.findOne(FeatureToggle, { identifier, deletedAt: null })\n\n if (!toggle) {\n const result: ToggleResolutionResult = {\n valueType: \"boolean\",\n value: null,\n source: \"missing\",\n toggleId: \"\",\n identifier,\n tenantId,\n }\n return result\n }\n\n let override: FeatureToggleOverride | null = null\n override = await this.em.findOne(FeatureToggleOverride, { toggle: toggle.id, tenantId })\n\n\n const result: ToggleResolutionResult = {\n valueType: toggle.type,\n value: override ? override.value : toggle.defaultValue,\n source: override ? \"override\" : \"default\",\n toggleId: toggle.id,\n identifier: toggle.identifier,\n tenantId,\n }\n\n await this.saveCache(identifier, tenantId, result)\n return result\n }\n\n public async invalidateIsEnabledCacheByIdentifierTag(identifier: string) {\n await this.cache.deleteByTags([getIdentifierTag(identifier)])\n }\n\n public async invalidateIsEnabledCacheByKey(identifier: string, tenantId: string) {\n await runWithCacheTenant(tenantId, () => this.cache.delete(getIsEnabledCacheKey(identifier, tenantId)))\n }\n\n public async getFeatureToggleValue<T>(\n identifier: string,\n ctx: ResolutionContext\n ): Promise<Result<T>> {\n const resolution = await this.resolveToggle(identifier, ctx.tenantId)\n\n if (resolution.source === \"missing\") {\n console.warn(`[feature_toggles] Toggle \"${identifier}\" not found (missing).`)\n return {\n ok: false,\n error: {\n code: \"MISSING_TOGGLE\",\n message: `Toggle \"${identifier}\" not found (missing).`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n\n\n if (resolution.valueType !== ctx.valueType) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"TYPE_MISMATCH\",\n message: `Toggle \"${identifier}\" has type \"${resolution.valueType}\" but \"${ctx.valueType}\" was requested.`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n const isValueValid =\n (ctx.valueType === \"boolean\" && typeof resolution.value === \"boolean\") ||\n (ctx.valueType === \"string\" && typeof resolution.value === \"string\") ||\n (ctx.valueType === \"number\" && typeof resolution.value === \"number\") ||\n (ctx.valueType === \"json\")\n\n if (!isValueValid) {\n console.error(\n `[feature_toggles] Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n { resolution }\n )\n return {\n ok: false,\n error: {\n code: \"INVALID_VALUE\",\n message: `Toggle \"${identifier}\" has invalid value for type \"${resolution.valueType}\".`,\n identifier,\n expectedType: ctx.valueType,\n actualType: resolution.valueType,\n source: resolution.source,\n },\n resolution,\n }\n }\n\n return {\n ok: true,\n value: resolution.value as T,\n resolution,\n }\n }\n\n public async getBoolConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<boolean>(identifier, { tenantId, valueType: \"boolean\" })\n }\n\n public async getNumberConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<number>(identifier, { tenantId, valueType: \"number\" })\n }\n\n public async getStringConfig(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<string>(identifier, { tenantId, valueType: \"string\" })\n }\n\n public async getJsonConfig<T = unknown>(identifier: string, tenantId: string) {\n return this.getFeatureToggleValue<T>(identifier, { tenantId, valueType: \"json\" })\n }\n}\n"],
5
+ "mappings": "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",
6
6
  "names": ["result"]
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@open-mercato/core",
3
- "version": "0.6.4-develop.3921.1.8a42ddf4c8",
3
+ "version": "0.6.4-develop.3929.1.fcf7afece2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "scripts": {
@@ -243,16 +243,16 @@
243
243
  "zod": "^4.4.3"
244
244
  },
245
245
  "peerDependencies": {
246
- "@open-mercato/ai-assistant": "0.6.4-develop.3921.1.8a42ddf4c8",
247
- "@open-mercato/shared": "0.6.4-develop.3921.1.8a42ddf4c8",
248
- "@open-mercato/ui": "0.6.4-develop.3921.1.8a42ddf4c8",
246
+ "@open-mercato/ai-assistant": "0.6.4-develop.3929.1.fcf7afece2",
247
+ "@open-mercato/shared": "0.6.4-develop.3929.1.fcf7afece2",
248
+ "@open-mercato/ui": "0.6.4-develop.3929.1.fcf7afece2",
249
249
  "react": "^19.0.0",
250
250
  "react-dom": "^19.0.0"
251
251
  },
252
252
  "devDependencies": {
253
- "@open-mercato/ai-assistant": "0.6.4-develop.3921.1.8a42ddf4c8",
254
- "@open-mercato/shared": "0.6.4-develop.3921.1.8a42ddf4c8",
255
- "@open-mercato/ui": "0.6.4-develop.3921.1.8a42ddf4c8",
253
+ "@open-mercato/ai-assistant": "0.6.4-develop.3929.1.fcf7afece2",
254
+ "@open-mercato/shared": "0.6.4-develop.3929.1.fcf7afece2",
255
+ "@open-mercato/ui": "0.6.4-develop.3929.1.fcf7afece2",
256
256
  "@testing-library/dom": "^10.4.1",
257
257
  "@testing-library/jest-dom": "^6.9.1",
258
258
  "@testing-library/react": "^16.3.1",
@@ -62,6 +62,13 @@ const getCacheTags = (identifier: string, tenantId: string) => {
62
62
 
63
63
  export class FeatureTogglesService {
64
64
  private cacheTtlMs: number = 1 * 60 * 1000 // 1 minute
65
+ // Resolution cache can be disabled via env (e.g. integration tests that flip
66
+ // overrides rapidly between cases). The 1-minute TTL is a production
67
+ // optimization; under fast flag churn it can serve a stale value across
68
+ // override set/clear despite invalidation, so tests opt out for determinism.
69
+ private readonly cacheDisabled: boolean =
70
+ process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === '1' ||
71
+ process.env.OM_FEATURE_TOGGLES_CACHE_DISABLED === 'true'
65
72
  constructor(
66
73
  private readonly cache: CacheService,
67
74
  private readonly em: EntityManager
@@ -72,6 +79,7 @@ export class FeatureTogglesService {
72
79
  tenantId: string,
73
80
  result: ToggleResolutionResult,
74
81
  ) {
82
+ if (this.cacheDisabled) return
75
83
  const key = getIsEnabledCacheKey(identifier, tenantId)
76
84
  await runWithCacheTenant(
77
85
  tenantId,
@@ -82,10 +90,12 @@ export class FeatureTogglesService {
82
90
  private async resolveToggle(identifier: string, tenantId: string): Promise<ToggleResolutionResult> {
83
91
  const key = getIsEnabledCacheKey(identifier, tenantId)
84
92
 
85
- const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))
86
- if (cached) {
87
- const parsed = toCachedResolution(cached)
88
- if (parsed) return parsed
93
+ if (!this.cacheDisabled) {
94
+ const cached = await runWithCacheTenant(tenantId, () => this.cache.get(key))
95
+ if (cached) {
96
+ const parsed = toCachedResolution(cached)
97
+ if (parsed) return parsed
98
+ }
89
99
  }
90
100
 
91
101
  let toggle: FeatureToggle | null = null