* Run one AppConfig poll for `entry`: starts a session if no token is held, then * calls `GetLatestConfiguration`. An empty payload means "unchanged" (or an * unseeded profile) and the previous value is kept. Any error is logged and the * last good value is retained. Marks the entry `loaded` on a
( ids: AppConfigProfileIdentifiers, parse: (json: unknown) => T, entry: CacheEntry<T> )
| 63 | * don't poll faster than the server allows (which would throttle). |
| 64 | */ |
| 65 | async function poll<T>( |
| 66 | ids: AppConfigProfileIdentifiers, |
| 67 | parse: (json: unknown) => T, |
| 68 | entry: CacheEntry<T> |
| 69 | ): Promise<T | null> { |
| 70 | let response: GetLatestConfigurationCommandOutput |
| 71 | try { |
| 72 | const dataClient = getClient() |
| 73 | |
| 74 | if (!entry.nextToken) { |
| 75 | const session = await dataClient.send( |
| 76 | new StartConfigurationSessionCommand({ |
| 77 | ApplicationIdentifier: ids.application, |
| 78 | EnvironmentIdentifier: ids.environment, |
| 79 | ConfigurationProfileIdentifier: ids.profile, |
| 80 | }) |
| 81 | ) |
| 82 | entry.nextToken = session.InitialConfigurationToken |
| 83 | } |
| 84 | |
| 85 | response = await dataClient.send( |
| 86 | new GetLatestConfigurationCommand({ ConfigurationToken: entry.nextToken }) |
| 87 | ) |
| 88 | entry.nextToken = response.NextPollConfigurationToken ?? entry.nextToken |
| 89 | } catch (error) { |
| 90 | // Network/session failure: drop the token so the next attempt starts a fresh |
| 91 | // session (handles expired or invalid tokens). Mark loaded + back off so we |
| 92 | // serve the fallback and retry in the background rather than blocking every |
| 93 | // request during an outage. |
| 94 | entry.nextToken = undefined |
| 95 | entry.expiresAt = Date.now() + DEFAULT_TTL_MS |
| 96 | entry.loaded = true |
| 97 | logger.error('AppConfig fetch failed; serving last known value', { |
| 98 | profile: cacheKey(ids), |
| 99 | error: getErrorMessage(error), |
| 100 | }) |
| 101 | return entry.value |
| 102 | } |
| 103 | |
| 104 | // Parse outside the network try: a decode/parse error must NOT discard the |
| 105 | // already-rotated session token — the round trip succeeded, so the next poll |
| 106 | // can reuse it instead of opening a new session. Keep the last good value. |
| 107 | try { |
| 108 | if (response.Configuration && response.Configuration.length > 0) { |
| 109 | const text = new TextDecoder().decode(response.Configuration) |
| 110 | entry.value = parse(JSON.parse(text)) |
| 111 | } |
| 112 | } catch (error) { |
| 113 | logger.error('AppConfig response parse failed; serving last known value', { |
| 114 | profile: cacheKey(ids), |
| 115 | error: getErrorMessage(error), |
| 116 | }) |
| 117 | } |
| 118 | |
| 119 | const intervalMs = (response.NextPollIntervalInSeconds ?? 60) * 1000 |
| 120 | entry.expiresAt = Date.now() + Math.max(DEFAULT_TTL_MS, intervalMs) |
| 121 | entry.loaded = true |
| 122 | return entry.value |
no test coverage detected