Skip to content

Add typed Flagger framework - #185

Open
kyleve wants to merge 3 commits into
mainfrom
codex/add-flagger-framework
Open

Add typed Flagger framework#185
kyleve wants to merge 3 commits into
mainfrom
codex/add-flagger-framework

Conversation

@kyleve

@kyleve kyleve commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Posted by an AI agent on kve's behalf.

Summary

  • add Flagger, a SwiftData-backed feature flag engine with typed groups, explicit module sources, JSON values, and default-eliding persistence
  • model launch-frozen, first-read-frozen, and live-updating behavior in the flag type, including synchronous cached reads and live value streams
  • add FlaggerUI with type-injected FlaggerModel access (flagger.myGroup.myFlag) and a searchable source/group editor
  • add focused unit coverage, editor snapshots, module documentation, and test-scheme wiring

Design notes

Each Flagger instance owns one scope and physical store, so app-wide, logged-in, and demo worlds compose as separate injected instances. Modules register their own FlagSource and group types; the root lists sources without knowing individual flags. Frozen flags accept editor overrides for the next applicable lifetime, while only LiveUpdating flags expose typed mutation and observation.

Feature flags change rarely, so each mutation saves one override and then fetches the complete override store. Persistence returns that full snapshot with one monotonic revision; the lock-backed synchronous cache atomically accepts only the newest revision and diffs snapshots to notify observers. Open-time overrides equal to their defaults are deleted in one batch.

Flagger itself is a Sendable class: all mutable cached state lives behind OSAllocatedUnfairLock, while the SwiftData context remains isolated in FlaggerPersistence. Registries retain source and group metatypes directly rather than exposing registration-wrapper types.

This adds the reusable modules but deliberately does not wire a Flagger instance into WhereServices yet.

Review focus

  • the lock-backed Sendable class and full-store revision handoff from the SwiftData actor
  • Mirror-based discovery of stored Flag properties when registered groups are opened
  • the dynamic-member/key-path API that preserves concrete group and flag types in SwiftUI

Testing

  • ./swiftformat --lint
  • ./test FlaggerTests FlaggerUITests (27 tests passed after the framework trim)
  • ./test --all (1,649 tests passed)
  • ./test --snapshots (34 tests passed)

Comment thread Shared/Flagger/Sources/Flagger.swift Outdated
let shouldNotify = state.withLock { state in
let appliedSequence = state.appliedCommitSequences[id] ?? 0
guard commit.sequence > appliedSequence else { return false }
state.appliedCommitSequences[id] = commit.sequence

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

This per-flag sequence ledger is load-bearing: Flagger can re-enter while awaiting the persistence actor, and actor resumptions are not FIFO. Keeping the applied sequence per FlagID prevents a later commit for one flag from causing an earlier successful commit for another flag to be discarded from the synchronous cache. The concurrent-write regression exercises this handoff.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

Superseded by 4a287d39. Persistence now returns the complete override store with one monotonic revision after every mutation. The synchronous cache accepts the newest full snapshot atomically and derives changed flag IDs by diffing, so the per-flag sequence ledger is gone.

public init() {}

public subscript<Group: FeatureFlagGroup>(_: Group.Type) -> Group {
Group()

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Putting a pin in this; seems a bit weird at first glance.

var failureObservers: [UUID: AsyncStream<FlaggerFailure>.Continuation] = [:]
}

private let state: OSAllocatedUnfairLock<State>

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Posted by an AI agent on kve's behalf.

This is now the framework's only in-memory concurrency boundary. Flagger is a Sendable class whose mutable state is entirely inside this lock; FlaggerPersistence independently serializes SwiftData work and returns complete versioned snapshots. Review should focus on keeping future mutable fields inside State rather than reintroducing actor-isolated or unsynchronized class state.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant