syncCacheState.ts 3.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596
  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. import { join } from 'path'
  24. import { getClaudeConfigHomeDir } from '../../utils/envUtils.js'
  25. import { readFileSync } from '../../utils/fileRead.js'
  26. import { stripBOM } from '../../utils/jsonRead.js'
  27. import { resetSettingsCache } from '../../utils/settings/settingsCache.js'
  28. import type { SettingsJson } from '../../utils/settings/types.js'
  29. import { jsonParse } from '../../utils/slowOperations.js'
  30. const SETTINGS_FILENAME = 'remote-settings.json'
  31. let sessionCache: SettingsJson | null = null
  32. let eligible: boolean | undefined
  33. export function setSessionCache(value: SettingsJson | null): void {
  34. sessionCache = value
  35. }
  36. export function resetSyncCache(): void {
  37. sessionCache = null
  38. eligible = undefined
  39. }
  40. export function setEligibility(v: boolean): boolean {
  41. eligible = v
  42. return v
  43. }
  44. export function getSettingsPath(): string {
  45. return join(getClaudeConfigHomeDir(), SETTINGS_FILENAME)
  46. }
  47. // sync IO — settings pipeline is sync. fileRead and jsonRead are leaves;
  48. // file.ts and json.ts both sit in the settings SCC.
  49. function loadSettings(): SettingsJson | null {
  50. try {
  51. const content = readFileSync(getSettingsPath())
  52. const data: unknown = jsonParse(stripBOM(content))
  53. if (!data || typeof data !== 'object' || Array.isArray(data)) {
  54. return null
  55. }
  56. return data as SettingsJson
  57. } catch {
  58. return null
  59. }
  60. }
  61. export function getRemoteManagedSettingsSyncFromCache(): SettingsJson | null {
  62. if (eligible !== true) return null
  63. if (sessionCache) return sessionCache
  64. const cachedSettings = loadSettings()
  65. if (cachedSettings) {
  66. sessionCache = cachedSettings
  67. // Remote settings just became available for the first time. Any merged
  68. // getSettings_DEPRECATED() result cached before this moment is missing
  69. // the policySettings layer (the `eligible !== true` guard above returned
  70. // null). Flush so the next merged read re-merges with this layer visible.
  71. //
  72. // Fires at most once: subsequent calls hit `if (sessionCache)` above.
  73. // When called from loadSettingsFromDisk() (settings.ts:546), the merged
  74. // cache is still null (setSessionSettingsCache runs at :732 after
  75. // loadSettingsFromDisk returns) — no-op. The async-fetch arm (index.ts
  76. // setSessionCache + notifyChange) already handles its own reset.
  77. //
  78. // gh-23085: isBridgeEnabled() at main.tsx Commander-definition time
  79. // (before preAction → init() → isRemoteManagedSettingsEligible()) reached
  80. // getSettings_DEPRECATED() at auth.ts:115. The try/catch in bridgeEnabled
  81. // swallowed the later getGlobalConfig() throw, but the merged settings
  82. // cache was already poisoned. See managedSettingsHeadless.int.test.ts.
  83. resetSettingsCache()
  84. return cachedSettings
  85. }
  86. return null
  87. }