/ services / remoteManagedSettings / syncCacheState.ts
syncCacheState.ts
 1  /**
 2   * Leaf state module for the remote-managed-settings sync cache.
 3   *
 4   * Split from syncCache.ts to break the settings.ts → syncCache.ts → auth.ts →
 5   * settings.ts cycle. auth.ts sits inside the large settings SCC; importing it
 6   * from settings.ts's own dependency chain pulls hundreds of modules into the
 7   * eagerly-evaluated SCC at startup.
 8   *
 9   * This module imports only leaves (path, envUtils, file, json, types,
10   * settings/settingsCache — also a leaf, only type-imports validation). settings.ts
11   * reads the cache from here. syncCache.ts keeps isRemoteManagedSettingsEligible
12   * (the auth-touching part) and re-exports everything from here for callers that
13   * don't care about the cycle.
14   *
15   * Eligibility is a tri-state here: undefined (not yet determined — return
16   * null), false (ineligible — return null), true (proceed). managedEnv.ts
17   * calls isRemoteManagedSettingsEligible() just before the policySettings
18   * read — after userSettings/flagSettings env vars are applied, so the check
19   * sees config-provided CLAUDE_CODE_USE_BEDROCK/ANTHROPIC_BASE_URL. That call
20   * computes once and mirrors the result here via setEligibility(). Every
21   * subsequent read hits the cached bool instead of re-running the auth chain.
22   */
23  
24  import { join } from 'path'
25  import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
26  import { readFileSync } from '../../utils/fileRead.js'
27  import { stripBOM } from '../../utils/jsonRead.js'
28  import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
29  import type { SettingsJson } from '../../utils/settings/types.js'
30  import { jsonParse } from '../../utils/slowOperations.js'
31  
32  const SETTINGS_FILENAME = 'remote-settings.json'
33  
34  let sessionCache: SettingsJson | null = null
35  let eligible: boolean | undefined
36  
37  export function setSessionCache(value: SettingsJson | null): void {
38    sessionCache = value
39  }
40  
41  export function resetSyncCache(): void {
42    sessionCache = null
43    eligible = undefined
44  }
45  
46  export function setEligibility(v: boolean): boolean {
47    eligible = v
48    return v
49  }
50  
51  export function getSettingsPath(): string {
52    return join(getClaudeConfigHomeDir(), SETTINGS_FILENAME)
53  }
54  
55  // sync IO — settings pipeline is sync. fileRead and jsonRead are leaves;
56  // file.ts and json.ts both sit in the settings SCC.
57  function loadSettings(): SettingsJson | null {
58    try {
59      const content = readFileSync(getSettingsPath())
60      const data: unknown = jsonParse(stripBOM(content))
61      if (!data || typeof data !== 'object' || Array.isArray(data)) {
62        return null
63      }
64      return data as SettingsJson
65    } catch {
66      return null
67    }
68  }
69  
70  export function getRemoteManagedSettingsSyncFromCache(): SettingsJson | null {
71    if (eligible !== true) return null
72    if (sessionCache) return sessionCache
73    const cachedSettings = loadSettings()
74    if (cachedSettings) {
75      sessionCache = cachedSettings
76      // Remote settings just became available for the first time. Any merged
77      // getSettings_DEPRECATED() result cached before this moment is missing
78      // the policySettings layer (the `eligible !== true` guard above returned
79      // null). Flush so the next merged read re-merges with this layer visible.
80      //
81      // Fires at most once: subsequent calls hit `if (sessionCache)` above.
82      // When called from loadSettingsFromDisk() (settings.ts:546), the merged
83      // cache is still null (setSessionSettingsCache runs at :732 after
84      // loadSettingsFromDisk returns) — no-op. The async-fetch arm (index.ts
85      // setSessionCache + notifyChange) already handles its own reset.
86      //
87      // gh-23085: isBridgeEnabled() at main.tsx Commander-definition time
88      // (before preAction → init() → isRemoteManagedSettingsEligible()) reached
89      // getSettings_DEPRECATED() at auth.ts:115. The try/catch in bridgeEnabled
90      // swallowed the later getGlobalConfig() throw, but the merged settings
91      // cache was already poisoned. See managedSettingsHeadless.int.test.ts.
92      resetSettingsCache()
93      return cachedSettings
94    }
95    return null
96  }