Add typed Flagger framework - #185
Conversation
| let shouldNotify = state.withLock { state in | ||
| let appliedSequence = state.appliedCommitSequences[id] ?? 0 | ||
| guard commit.sequence > appliedSequence else { return false } | ||
| state.appliedCommitSequences[id] = commit.sequence |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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() |
There was a problem hiding this comment.
Putting a pin in this; seems a bit weird at first glance.
| var failureObservers: [UUID: AsyncStream<FlaggerFailure>.Continuation] = [:] | ||
| } | ||
|
|
||
| private let state: OSAllocatedUnfairLock<State> |
There was a problem hiding this comment.
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.
Summary
Flagger, a SwiftData-backed feature flag engine with typed groups, explicit module sources, JSON values, and default-eliding persistenceFlaggerUIwith type-injectedFlaggerModelaccess (flagger.myGroup.myFlag) and a searchable source/group editorDesign notes
Each
Flaggerinstance owns one scope and physical store, so app-wide, logged-in, and demo worlds compose as separate injected instances. Modules register their ownFlagSourceand group types; the root lists sources without knowing individual flags. Frozen flags accept editor overrides for the next applicable lifetime, while onlyLiveUpdatingflags 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.
Flaggeritself is aSendableclass: all mutable cached state lives behindOSAllocatedUnfairLock, while the SwiftData context remains isolated inFlaggerPersistence. 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
WhereServicesyet.Review focus
Sendableclass and full-store revision handoff from the SwiftData actorFlagproperties when registered groups are openedTesting
./swiftformat --lint./test FlaggerTests FlaggerUITests(27 tests passed after the framework trim)./test --all(1,649 tests passed)./test --snapshots(34 tests passed)