* Process a remote eval payload from the GrowthBook server and populate * local caches. Called after both initial client.init() and after * client.refreshFeatures() so that _BLOCKS_ON_INIT callers see fresh values * across the process lifetime, not just init-time snapshots. * * Without this run
( gbClient: GrowthBook, )
| 325 | * kill switch for long-running sessions. |
| 326 | */ |
| 327 | async function processRemoteEvalPayload( |
| 328 | gbClient: GrowthBook, |
| 329 | ): Promise<boolean> { |
| 330 | // WORKAROUND: Transform remote eval response format |
| 331 | // The API returns { "value": ... } but SDK expects { "defaultValue": ... } |
| 332 | // TODO: Remove this once the API is fixed to return correct format |
| 333 | const payload = gbClient.getPayload() |
| 334 | // Empty object is truthy — without the length check, `{features: {}}` |
| 335 | // (transient server bug, truncated response) would pass, clear the maps |
| 336 | // below, return true, and syncRemoteEvalToDisk would wholesale-write `{}` |
| 337 | // to disk: total flag blackout for every process sharing ~/.claude.json. |
| 338 | if (!payload?.features || Object.keys(payload.features).length === 0) { |
| 339 | return false |
| 340 | } |
| 341 | |
| 342 | // Clear before rebuild so features removed between refreshes don't |
| 343 | // leave stale ghost entries that short-circuit getFeatureValueInternal. |
| 344 | experimentDataByFeature.clear() |
| 345 | |
| 346 | const transformedFeatures: Record<string, MalformedFeatureDefinition> = {} |
| 347 | for (const [key, feature] of Object.entries(payload.features)) { |
| 348 | const f = feature as MalformedFeatureDefinition |
| 349 | if ('value' in f && !('defaultValue' in f)) { |
| 350 | transformedFeatures[key] = { |
| 351 | ...f, |
| 352 | defaultValue: f.value, |
| 353 | } |
| 354 | } else { |
| 355 | transformedFeatures[key] = f |
| 356 | } |
| 357 | |
| 358 | // Store experiment data for later logging when feature is accessed |
| 359 | if (f.source === 'experiment' && f.experimentResult) { |
| 360 | const expResult = f.experimentResult as { |
| 361 | variationId?: number |
| 362 | } |
| 363 | const exp = f.experiment as { key?: string } | undefined |
| 364 | if (exp?.key && expResult.variationId !== undefined) { |
| 365 | experimentDataByFeature.set(key, { |
| 366 | experimentId: exp.key, |
| 367 | variationId: expResult.variationId, |
| 368 | }) |
| 369 | } |
| 370 | } |
| 371 | } |
| 372 | // Re-set the payload with transformed features |
| 373 | await gbClient.setPayload({ |
| 374 | ...payload, |
| 375 | features: transformedFeatures, |
| 376 | }) |
| 377 | |
| 378 | // WORKAROUND: Cache the evaluated values directly from remote eval response. |
| 379 | // The SDK's evalFeature() tries to re-evaluate rules locally, ignoring the |
| 380 | // pre-evaluated 'value' from remoteEval. setForcedFeatures also doesn't work |
| 381 | // reliably. So we cache values ourselves and use them in getFeatureValueInternal. |
| 382 | remoteEvalFeatureValues.clear() |
| 383 | for (const [key, feature] of Object.entries(transformedFeatures)) { |
| 384 | // Under remoteEval:true the server pre-evaluates. Whether the answer |
no test coverage detected