RemoteConfigStore

0.4.0

AltiAntonov/RemoteConfigStore

What's New

0.4.0

2026-04-12T07:34:45Z

HTTP cache validation release.

Added

  • persisted HTTP validation metadata for cached snapshots
  • conditional request header support for If-None-Match and If-Modified-Since
  • explicit 304 Not Modified handling in the built-in HTTP fetcher
  • cache revalidation support that renews freshness without replacing an unchanged payload
  • dedicated HTTP Cache Validation example scenario in the showcase app

Changed

  • the built-in HTTP fetcher now captures ETag and Last-Modified response headers
  • the store now reuses cached payload values when HTTP revalidation reports 304 Not Modified
  • README and DocC now document HTTP cache validation behavior and example coverage

What's Changed

Full Changelog: 0.3.1...0.4.0

RemoteConfigStore

Offline-first remote config caching with TTL, stale fallback, and typed keys.

Swift version compatibility Platform compatibility MIT License Swift workflow

Features · Installation · Quick Start · When To Use · Good Fits · Weaker Fits · Read Policies · Freshness Model · Errors · Example App · Documentation · Testing · Example Scenarios

Features

  • memory and disk-backed cache layers
  • TTL-based freshness
  • optional stale fallback using maxStaleAge
  • injected fetcher protocol for remote loading
  • typed key access for primitive values
  • actor-backed store implementation for serialized state access

The public API is intentionally centered on:

  • RemoteConfigStore
  • RemoteConfigFetcher
  • RemoteConfigSnapshot
  • RemoteConfigRefreshResult
  • RemoteConfigKey
  • RemoteConfigValue
  • ReadPolicy
  • RemoteConfigStoreError
  • Logger

Installation

Add RemoteConfigStore to your Swift Package Manager dependencies:

dependencies: [
    .package(url: "https://github.com/AltiAntonov/RemoteConfigStore.git", from: "0.4.0")
]

Then add the product to your target:

.target(
    name: "YourApp",
    dependencies: [
        .product(name: "RemoteConfigStore", package: "RemoteConfigStore")
    ]
)

If you need to try unreleased changes, pin to a branch or revision explicitly.

Quick Start

import Foundation
import RemoteConfigStore

enum AppConfigKeys {
    static let newUI = RemoteConfigKey<Bool>("new_ui", defaultValue: false)
}

struct AppFetcher: RemoteConfigFetcher {
    func fetchSnapshot() async throws -> RemoteConfigSnapshot {
        RemoteConfigSnapshot(values: [
            "new_ui": .bool(true)
        ])
    }
}

let directory = FileManager.default.urls(for: .cachesDirectory, in: .userDomainMask)[0]
let store = try RemoteConfigStore(
    fetcher: AppFetcher(),
    cacheDirectory: directory.appendingPathComponent("RemoteConfigStore"),
    ttl: 300,
    maxStaleAge: 3600
)

let enabled = try await store.value(for: AppConfigKeys.newUI, using: .immediate)

If you need to know whether a refresh actually changed the payload, use refreshResult():

switch try await store.refreshResult() {
case .updated(let snapshot):
    print("Config changed:", snapshot.values)
case .unchanged:
    print("Config payload is unchanged.")
}

You can also use the built-in HTTP path when your config comes from a JSON endpoint:

let store = try RemoteConfigStore(
    request: HTTPRemoteConfigRequest(
        url: URL(string: "https://example.com/remote-config.json")!,
        headers: ["Authorization": "Bearer token"],
        timeoutInterval: 8
    ),
    cacheDirectory: directory.appendingPathComponent("RemoteConfigStore"),
    ttl: 300
)

When the built-in HTTP fetcher receives ETag or Last-Modified headers, the store persists that validation metadata and uses it for later conditional refreshes.

When To Use RemoteConfigStore

Use RemoteConfigStore when an app needs server-driven values but should still behave predictably when the network is slow, unavailable, or temporarily failing.

It is a strong fit for configuration that should be cached locally, refreshed deliberately, and read through a typed API instead of raw dictionaries spread across an app.

Good Fits

  • Feature flags and staged rollout controls Example: Feature Flags scenario
  • Runtime tuning values such as request timeouts, polling intervals, and rollout percentages Example: Feature Flags scenario
  • Remote text or copy that should remain available offline Example: Feature Flags scenario
  • Safety switches and operational config that benefit from stale fallback instead of hard failure Example soon
  • Apps that care about startup speed and want cache-first or stale-while-revalidate reads Example: Feature Flags scenario

Weaker Fits

  • Deeply nested or highly structured configuration documents Example soon
  • Cases where config must always be fresh and stale data is never acceptable Example soon
  • Projects that need a built-in HTTP client, ETag validation, or observer streams today Example: HTTP Cache Validation scenario
  • Data that is really user content or secure secret material rather than app configuration Example soon

Read Policies

RemoteConfigStore currently supports three read policies:

Policy Fresh cache Stale but usable cache No usable cache Best fit
.immediate returns cache returns cache waits for refresh fast UI reads
.refreshBeforeReturning returns cache tries refresh first waits for refresh freshest possible values
.immediateWithBackgroundRefresh returns cache returns cache and refreshes in background waits for refresh responsive reads with silent catch-up

.immediate

Return a usable cached snapshot immediately when one exists.

  • fresh cache: returned immediately
  • stale but still usable cache: returned immediately
  • no usable cache: refresh is attempted and its result is returned
  • refresh failure with no usable cache: throws

Use this when UI responsiveness matters more than eagerly refreshing stale data.

.refreshBeforeReturning

Prefer a refresh before returning stale data.

  • fresh cache: returned immediately
  • stale cache: refresh is attempted first
  • refresh failure with still-usable stale cache: stale data is returned
  • refresh failure with no usable cache: throws

Use this when you want the newest possible config before continuing, but still need an offline fallback.

.immediateWithBackgroundRefresh

Return usable cached data now and refresh asynchronously in the background.

  • fresh or stale-but-usable cache: returned immediately
  • a background refresh is scheduled
  • no usable cache: refresh is awaited directly

Use this when you want instant reads while still nudging the cache toward freshness.

Freshness Model

The store uses two time windows:

  • ttl: how long a fetched snapshot is considered fresh
  • maxStaleAge: an optional extra window during which expired data may still be served

That creates three states:

  1. fresh The cache is within ttl and can be returned without qualification.

  2. stale but usable The cache is older than ttl, but still within maxStaleAge.

  3. expired and unusable The cache is older than both windows and must not be used as a fallback.

Errors

RemoteConfigStore keeps its store-specific error surface intentionally small.

  • RemoteConfigStoreError.noCachedSnapshot: returned when neither memory nor disk cache contains a snapshot

Other failures are propagated from the underlying component that failed:

  • refresh() and snapshot(using:) can throw the injected fetcher's error
  • refresh() can throw cache persistence or file-system errors from disk writes
  • cachedSnapshot() can throw file-system errors while loading from disk
  • RemoteConfigStore.init(...) can throw when the cache directory cannot be prepared

Example App

The repository includes an Xcode example app in Example/RemoteConfigStore.

Current scenarios:

  • Feature Flags Code: FeatureFlagsDemoView.swift Shows typed keys, all three read policies, manual refreshes, revision drift, and the difference between typed accessors and the raw cached payload.
  • HTTP Fetcher Code: HTTPFetcherDemoView.swift Shows the built-in URLSession fetcher, URL-based store construction, typed-key reads, and refresh-result reporting.
  • HTTP Cache Validation Code: HTTPFetcherDemoView.swift Shows persisted ETag metadata, conditional requests, 304 Not Modified, and cache freshness renewal without a payload change.

Planned scenarios:

  • Offline Fallback - Example soon
  • Observers And Metrics - Example soon

Documentation

The package now includes a DocC catalog in Sources/RemoteConfigStore/RemoteConfigStore.docc.

Once Swift Package Index processes the .spi.yml manifest and the DocC catalog, hosted documentation should appear on the package page automatically.

Current DocC coverage includes:

  • getting started with injected fetchers
  • read policy behavior
  • built-in HTTP cache validation behavior

Example Scenarios

The example app is designed as one showcase application with multiple focused scenarios rather than many separate demo projects.

Implemented:

Coming later:

  • Offline Fallback - Example soon
  • Structured Payloads - Example soon

If you create a fresh example app manually in the future, recommended Xcode options are:

  • template: iOS App
  • interface: SwiftUI
  • language: Swift
  • testing: Swift Testing
  • Core Data: No
  • development team: unset unless signing is needed locally

Testing

Package tests now use Swift Testing, not XCTest.

Current structure:

  • Tests/RemoteConfigStoreTests/Models
  • Tests/RemoteConfigStoreTests/Cache
  • Tests/RemoteConfigStoreTests/Store
  • Tests/RemoteConfigStoreTests/Support

Run the package tests with:

swift test

See CHANGELOG.md for released changes.

Description

  • Swift Tools 6.0.0
View More Packages from this Author

Dependencies

  • None
Last updated: Sun Apr 19 2026 17:37:19 GMT-0900 (Hawaii-Aleutian Daylight Time)