Layer a local config beneath the remote one, per environment - #42
Merged
Conversation
An app that talks to more than one backend usually wants two config layers for the
environment it is in: one bundled with the build, one fetched. EnvironmentConfiguration
holds both and keeps them in step with the environment you switch to.
let config = EnvironmentConfiguration(
AppFlags.self,
local: { env in Bundle.main.data(named: "\(env).json") },
remote: { env in try await api.fetchConfig(for: env) }
)
let pole = FlagPole(AppFlags.self, sources: [byHand] + config.sources)
await config.load(.staging) // local staging.json, then remote staging.json
Precedence within the pair is remote over local, so the full stack highest-first is a
by-hand override, the fetched config, the bundled config, then the compiled defaults.
Loading an environment clears each layer before loading it, so a fetch that fails or
returns nothing falls back to the layer beneath rather than leaving an app labelled
staging running the previous environment's values. load(_:) returns a LoadOutcome
saying what happened to each layer, since one can succeed while the other does not.
The framework does no networking and reads no files — the two closures hand it the
bytes, it decodes, validates, and layers them. Each layer is an ordinary
RemoteOverrideSource, so either can be audited with a FlagMappingAudit.
The demo is wired through it: a bundled local config per environment beneath the
fetched one, and clearing the remote layer now falls back to the local config rather
than to raw defaults — the local layer earning its place.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
The demo's on-screen copy still described a single fetched payload. It now says the environment loads two layers — a bundled local config, then a fetched remote one, remote winning — and the provenance footer names the whole stack, highest first. Clearing the remote layer points at the local fallback the sources panel then shows. The docs section gains the missing half of the story: binding the load to the environment flag's publisher, so setting the environment to staging loads staging's layers, and the note on why that flag must carry no remoteKey. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
Switching environments quickly, with a slow fetch, could leave the new environment showing the old one's values: two overlapping load calls each clear then apply, and whichever apply landed last won regardless of which environment was current. That is the exact failure clearing-first exists to prevent, reached from the other side. Each load now takes a monotonic epoch and re-checks it after the fetch: a result whose epoch is no longer current is dropped, reported as a new LayerOutcome.superseded rather than written over the newer load. Found reviewing the coordinator. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
An app that talks to more than one backend usually wants two config layers for the
environment it is in: one bundled with the build, one fetched.
EnvironmentConfigurationholds both and keeps them in step with the environment you switch to.
Precedence
Highest wins:
Remote wins over local for the same flag;
config.sourcesreturns them in that order.Clear-first, so a failure falls back rather than lies
Loading an environment clears each layer before it loads it. A fetch that fails or
returns nothing leaves that layer empty and the one beneath showing through — the
bundled config, then the defaults — rather than leaving an app labelled staging
running the previous environment's values.
load(_:)returns aLoadOutcomesayingwhat happened to each layer, since one can succeed while the other does not.
The framework does no networking and reads no files: the two closures hand it the
bytes, it decodes, validates against the schema, and layers them. Each layer is an
ordinary
RemoteOverrideSource, so either can be audited with aFlagMappingAudit.Adversarial review
One defect, and it was the clear-first failure reached from the other side. Two
overlapping
loadcalls — switch a→b with a slow a fetch — each cleared then applied,and whichever apply landed last won regardless of which environment was current, so b
could end up showing a's values. Each load now carries a monotonic epoch and re-checks
it after the fetch; a stale result is dropped and reported as
LayerOutcome.supersededrather than written over the newer load.
Clean under review: empty
Data()reads as a failure (not valid JSON), not absent;both-absent is complete.
Demo
Wired through it: a bundled local config per environment beneath the fetched one, driven
by the environment flag. Clearing the remote layer falls back to the local config, not
raw defaults, and the on-device provenance panel shows the sources flip Remote → Local.
Verified
771 tests. DocC clean with
--warnings-as-errors, both example apps and the iOS packagescheme build, and the layering was walked through on a simulator: switching environments,
remote-over-local, and the clear-to-local fallback.
🤖 Generated with Claude Code
https://claude.ai/code/session_01NnwCqKNm8rZ63Au6bpYdaD