Skip to content

feat(KlaviyoCore): add IdentityStore and SDKConfigStore (MAGE-748)#617

Merged
belleklaviyo merged 4 commits into
feat/identity-to-corefrom
bl/identitystore-and-sdkconfigstore
Jun 22, 2026
Merged

feat(KlaviyoCore): add IdentityStore and SDKConfigStore (MAGE-748)#617
belleklaviyo merged 4 commits into
feat/identity-to-corefrom
bl/identitystore-and-sdkconfigstore

Conversation

@belleklaviyo

@belleklaviyo belleklaviyo commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

Description

Adds two singleton observable stores to KlaviyoCoreIdentityStore (profile identity) and SDKConfigStore (SDK configuration, starting with the company API key) — so other modules can observe identity/API-key state without importing KlaviyoSwift. Additive only; nothing is wired up or migrated in this PR.

Due Diligence

  • I have tested this on a simulator or a physical device.
  • I have added sufficient unit/integration tests of my changes.
  • I have adjusted or added new test cases to team test docs, if applicable.
  • I am confident these changes are compatible with all iOS and XCode versions the SDK currently supports.

Release/Versioning Considerations

  • Minor Contains changes to the public API.
  • This is planned work for an upcoming release.

New public API surface is added to KlaviyoCore but no existing behavior changes. The stores are not yet read or written by any module — that happens in the follow-up tickets below.

Changelog / Code Overview

Part of Phase 1 of the Networking Modularization spec, which moves identity + API-key state out of the KlaviyoSwift monolith into KlaviyoCore so KlaviyoForms and KlaviyoLocation no longer import KlaviyoSwift just to observe that state.

New files

  • Sources/KlaviyoCore/IdentityStore.swift
    • IdentityReading (current, publisher, stream()) / IdentityWriting (update(_:)) protocols
    • IdentityStore singleton (.shared) — source of truth for ProfileData
  • Sources/KlaviyoCore/SDKConfigStore.swift
    • ConfigReading (apiKey, apiKeyPublisher) / ConfigWriting (updateAPIKey(_:)) protocols
    • SDKConfigStore singleton (.shared) — source of truth for the company API key
  • Unit tests for both stores in Tests/KlaviyoCoreTests/

Design decisions (resolved during implementation, per the ticket)

  • Two stores, not one. The API key is SDK configuration (set once at init, ~one per app, stable for the app lifetime), not profile identity (per-user, frequently nil, changes on identify/reset). Splitting them gives honest naming, lets API-key-only consumers (CompanyObserver, KlaviyoLocationManager) depend on config alone, and gives future config (region/host, feature flags) a home. This overrides the spec's original single bundled IdentityStore.
  • Read/write protocol split: adopted. Consumers depend only on the read interface; the writing side stays with KlaviyoSwift. This documents the one-directional dependency (KlaviyoSwift writes, everyone else reads) and lets the implementation change later (e.g. become an actor) without touching consumers. Tests include MockIdentityReader / MockConfigReader that conform to the reading protocols only — a compile-time proof that read consumers get no write access.
  • @_spi write enforcement: deferred. Compiler-level enforcement of the write boundary (so same-package KlaviyoForms/KlaviyoLocation can't call writes) is deferred to a possible follow-up commit on this branch; the protocol split already documents intent.
  • Thread safety via serial DispatchQueue, not an actor. Reads are synchronous by design (current / apiKey), matching how consumers will use them; an actor would force await on every read and break that contract. The protocol split keeps an actor migration open for later if async reads become acceptable.

How this fits the broader modularization

Ticket Role
MAGE-747 ✅ Promote ProfileData + SDKError to KlaviyoCore (merged — blocker for this PR)
MAGE-748 (this PR) Add IdentityStore + SDKConfigStore to KlaviyoCore — the shared source of truth
MAGE-749 Refactor KlaviyoState to compose ProfileData; wire KlaviyoInternal to push state into these stores
MAGE-750 Migrate KlaviyoForms / KlaviyoLocation observers to read from these stores, dropping their KlaviyoSwift import for identity/config

Once consumers read from KlaviyoCore (749 + 750), KlaviyoSwift internals (TCA store, KlaviyoState shape, queue, flush) become private implementation details — unblocking Phase 2 (Event to KlaviyoCore) and Phase 3 (queue/retry/networking extraction) as module-contained changes.

Test Plan

make test-library (run via xcodebuild test on iPhone 17 Pro simulator). New tests, all passing:

  • Initial state: IdentityStore.current is empty ProfileData, SDKConfigStore.apiKey is nil
  • update(_:) / updateAPIKey(_:) reflect synchronously on current / apiKey
  • Updates emit on publisher / apiKeyPublisher (current value replayed on subscribe, then the update)
  • stream() emits updates
  • stream() delivers all updates to concurrent consumers with no dropped emissions (100 updates × 2 consumers)
  • MockIdentityReader / MockConfigReader compile while conforming to the reading protocols only

Related Issues/Tickets

Summary by CodeRabbit

  • New Features
    • Added an identity management store that exposes the current user profile plus reactive updates through publisher-style and async streaming interfaces.
    • Added an SDK configuration store for the API key with synchronous access and real-time update streams.
  • Tests
    • Added unit tests verifying initial state, synchronous updates, replay behavior for publisher/async streams, and correct concurrent stream consumption.

Introduce two singleton observable stores in KlaviyoCore, readable by any
module without importing KlaviyoSwift: IdentityStore (source of truth for
ProfileData) and SDKConfigStore (source of truth for the company API key).

Each store exposes a read/write protocol split (IdentityReading/IdentityWriting,
ConfigReading/ConfigWriting) so consumers depend only on the read interface.
Thread safety via a serial DispatchQueue, preserving synchronous reads.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 16, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 845091af-6bd9-4bf3-b0a4-a405f82c45f5

📥 Commits

Reviewing files that changed from the base of the PR and between 625c9b6 and 9c66166.

📒 Files selected for processing (1)
  • Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift

📝 Walkthrough

Walkthrough

Two new Combine-backed store modules are added to KlaviyoCore: IdentityStore manages ProfileData identity state, and SDKConfigStore manages the SDK API key configuration. Both follow the same pattern of segregated read/write protocols, a CurrentValueSubject-backed concrete class with a shared singleton, and XCTest suites validating synchronous, publisher, and async-stream behaviors.

Changes

KlaviyoCore Store Modules

Layer / File(s) Summary
IdentityStore protocols, implementation, and tests
Sources/KlaviyoCore/IdentityStore.swift, Tests/KlaviyoCoreTests/IdentityStoreTests.swift
Defines IdentityReading/IdentityWriting protocols and IdentityStore backed by CurrentValueSubject<ProfileData, Never>, exposing synchronous current, a Combine publisher, an AsyncStream-based stream(), and update(_:). Tests cover initial state, synchronous update, publisher replay, async stream replay, two concurrent consumers, and a MockIdentityReader compile-time read-only conformance check.
SDKConfigStore model, protocols, implementation, and tests
Sources/KlaviyoCore/SDKConfigStore.swift, Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift
Defines KlaviyoConfig data model with optional apiKey, ConfigReading/ConfigWriting protocols, and SDKConfigStore final class backed by CurrentValueSubject<KlaviyoConfig, Never>, exposing synchronous current, publisher, stream(), and update(_:). Tests verify nil initial state, synchronous mutation, publisher emission order, async stream behavior, and a MockConfigReader compile-time read-only conformance check.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~12 minutes

Possibly related PRs

Suggested reviewers

  • dan-peluso
  • ndurell

Poem

🐇 Two new stores hop into core,
Identity and config at the door.
A subject holds state, publishers share light,
AsyncStreams flow through the night.
Read and write protocols split with care —
This bunny approves what's added there! 🌟

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main change: adding two new stores (IdentityStore and SDKConfigStore) to KlaviyoCore, with a concise reference to the ticket.
Description check ✅ Passed The PR description is comprehensive and well-structured, covering all major template sections with detailed explanations of the changes, design decisions, testing approach, and broader architectural context.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch bl/identitystore-and-sdkconfigstore

Comment @coderabbitai help to get the list of available commands and usage tips.

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 8a91b8a. Configure here.

Comment thread Sources/KlaviyoCore/IdentityStore.swift Outdated
…s reads

Wrapping CurrentValueSubject.send in queue.sync delivered subscriber closures
synchronously on the store queue, so any subscriber reading current/apiKey in
response deadlocked (DispatchQueue.sync is non-reentrant). CurrentValueSubject
is already internally synchronized, so the external queue is removed.

Co-Authored-By: Claude Opus 4 (1M context) <noreply@anthropic.com>
@belleklaviyo

Copy link
Copy Markdown
Contributor Author

Before:

            observe identity + apiKey
   ┌─────────────────────────────────-────────────┐
   │                                              │
   ▼                                              │
┌─────────────────────────────────-─┐             │
│           KlaviyoSwift            │             │
│  (TCA store, KlaviyoState, queue, │             │
│   flush, networking, analytics)   │             │
│                                   │             │
│   KlaviyoInternal                 │◄────────────┤
│     • apiKeyPublisher()           │             │
│     • profileChangePublisher()    │◄──────┐     │
│     • fetchAPIKey()               │       │     │
└──────────────┬────────────────────┘       │     │
               │ imports                     │ imports
               ▼                             │     │
        ┌─────────────┐              ┌───────┴──────────┐
        │ KlaviyoCore │              │  KlaviyoForms    │
        │ ProfileData │              │  KlaviyoLocation │
        │ SDKError    │              └──────────────────┘
        └─────────────┘
            ▲
            └── everyone already depends on Core

   Problem: to read 2 small bits of state, Forms & Location pull in
   ALL of KlaviyoSwift (TCA + queue + networking) as a transitive dep.
   Any refactor of KlaviyoSwift internals risks breaking them.

After:

┌────────────────────────────────────┐
│            KlaviyoSwift             │
│  (TCA store, KlaviyoState, queue…)  │
│                                     │
│  KlaviyoInternal ── push state ──┐  │   (write side only)
└──────────────────────────────────┼──┘
               │ imports            │
               ▼                    ▼ update(_:) / updateAPIKey(_:)
        ┌──────────────────────────────────────────┐
        │                KlaviyoCore                │
        │                                           │
        │   IdentityStore        SDKConfigStore     │
        │   • current            • apiKey           │
        │   • publisher          • apiKeyPublisher  │
        │   • stream()                              │
        │   [IdentityReading]    [ConfigReading]    │  ◄── read-only protocols
        └──────────────────────────────────────────┘
               ▲                    ▲
               │ read identity      │ read apiKey
               │                    │
        ┌──────┴──────────┐  ┌──────┴───────────┐
        │  KlaviyoForms   │  │ KlaviyoLocation  │
        └─────────────────┘  └──────────────────┘
        (no more `import KlaviyoSwift` for identity/config)

   Result: KlaviyoCore is the stable shared contract.
   KlaviyoSwift's internals become private implementation details,
   free to be restructured (Phase 2/3) without touching Forms/Location.

@belleklaviyo belleklaviyo marked this pull request as ready for review June 18, 2026 14:06
@belleklaviyo belleklaviyo requested a review from a team as a code owner June 18, 2026 14:06
@klaviyoit klaviyoit requested a review from ndurell June 18, 2026 14:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/KlaviyoCore/SDKConfigStore.swift`:
- Around line 22-45: Create a reusable generic utility class or extension (such
as ObservableValueStore<T>) that encapsulates the CurrentValueSubject pattern
with its associated getter, publisher, and update method. This utility should
handle the internal synchronization and Combine emission logic currently
duplicated in SDKConfigStore. Replace the apiKeySubject property and its related
methods (apiKey getter, apiKeyPublisher, and updateAPIKey) in SDKConfigStore
with a single composed instance of ObservableValueStore<String?>, following the
same pattern that IdentityStore should also adopt to eliminate duplication and
maintain consistent concurrency behavior across both classes.

In `@Tests/KlaviyoCoreTests/IdentityStoreTests.swift`:
- Around line 78-85: The tuple binding at line 80 uses single-letter variable
names `a` and `b` which violate SwiftLint's identifier_name rules. Rename these
variables to more descriptive names that meet SwiftLint's minimum identifier
length requirements (typically avoiding single-letter names), and update all
references to `a` and `b` in the subsequent XCTAssertEqual assertions to use the
new variable names.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: 7ed4541f-2aaa-43f1-a7ef-d0f51ae117e4

📥 Commits

Reviewing files that changed from the base of the PR and between cc67bb7 and 3ab4ad8.

📒 Files selected for processing (4)
  • Sources/KlaviyoCore/IdentityStore.swift
  • Sources/KlaviyoCore/SDKConfigStore.swift
  • Tests/KlaviyoCoreTests/IdentityStoreTests.swift
  • Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift

Comment thread Sources/KlaviyoCore/SDKConfigStore.swift
Comment thread Tests/KlaviyoCoreTests/IdentityStoreTests.swift
Comment thread Sources/KlaviyoCore/SDKConfigStore.swift Outdated
Replace the single apiKey surface on ConfigReading/ConfigWriting with a
KlaviyoConfig value type, mirroring the IdentityStore/ProfileData pattern.
This lets future config settings (enabled features, data-privacy consent)
be added without changing the protocol surface.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift`:
- Line 22: The API key string literal "company-123" is repeated in multiple
places within the test (used on lines 22 and 34 in the store.update calls with
KlaviyoConfig). Extract this repeated literal into a shared test constant at the
top of the SDKConfigStoreTests class, then replace both occurrences of the
hardcoded string with references to this constant to improve maintainability and
consistency.
- Around line 52-54: Replace the Task.sleep call that introduces timing
dependency with a deterministic approach by awaiting the stream's initial value
directly after subscription. Remove the sleep statement entirely and instead
await the initial replay from the stream subscription before calling
store.update with the new KlaviyoConfig, then assert on the stream's emitted
value to ensure the update was received. This eliminates the race condition and
makes the test deterministic regardless of CI worker speed.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Enterprise

Run ID: faf2033b-6c47-4a2e-b69d-4090365d08af

📥 Commits

Reviewing files that changed from the base of the PR and between 3ab4ad8 and 625c9b6.

📒 Files selected for processing (2)
  • Sources/KlaviyoCore/SDKConfigStore.swift
  • Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift

Comment thread Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift Outdated
Comment thread Tests/KlaviyoCoreTests/SDKConfigStoreTests.swift Outdated
- Extract repeated apiKey literal into a shared test constant
- Replace Task.sleep timing dependency in the stream test with a
  deterministic async iterator to avoid CI flakiness

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@belleklaviyo belleklaviyo merged commit 9ac6a7d into feat/identity-to-core Jun 22, 2026
18 checks passed
@belleklaviyo belleklaviyo deleted the bl/identitystore-and-sdkconfigstore branch June 22, 2026 18:36
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.

3 participants