Skip to content

0.1.2

  • 0.1.2
  • 6416b66
  • Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
  • Choose a tag to compare

  • 0.1.2
  • 6416b66
  • Choose a tag to compare

  • Verified

    This commit was created on GitHub.com and signed with GitHub’s verified signature.
@tautobot tautobot tagged this 27 Jun 08:05
First PUBLIC streaming surface on ``Pyrx``. Native iOS apps can now
subscribe to a closed taxonomy of SDK events (``PyrxEvent``) without
re-implementing ``UNUserNotificationCenter`` delegation or polling
SDK state. The React Native bridge in ``@pyrx/synapse-react-native``
will collect from this same surface in Phase 9.2.1 PR-3, making
``usePushReceived`` / ``usePushClicked`` / ``useDeepLink`` /
``useIdentityChanged`` functional in v0.2.0.

Per the Phase 9.2.1 plan (in upstream ``pyrx.synapse``,
``docs/plans/phase-9.2.1-native-callback-observers-plan-2026-06-27.md``).

## Public surface

  * ``Pyrx.shared.observe(on:_:) -> PyrxObserverToken`` — closure-based
    observer registration. Multi-subscriber; tokens are independent;
    handler runs on the queue passed as ``on:`` (default ``.main``).
  * ``Pyrx.shared.events() -> AsyncStream<PyrxEvent>`` — AsyncStream
    sugar for Swift Concurrency-first callers. Each call returns a
    fresh stream; cancelling the consuming ``Task`` cancels the
    underlying token.
  * ``PyrxEvent`` enum with 5 cases (D4):
      - ``.pushReceived(PushReceivedEvent)``
      - ``.pushClicked(PushClickedEvent)``  (NOT cold-start)
      - ``.pushReceivedColdStart(PushReceivedEvent)``
      - ``.queueDrained(count: Int)``  (only when count > 0)
      - ``.identityChanged(before: IdentitySnapshot, after: IdentitySnapshot)``
  * ``PyrxObserverToken`` — opaque reference-type handle with idempotent
    ``cancel()``. Holding the token keeps the subscription alive; drop
    the only reference and the deinit cancels.
  * ``PyrxAttributeValue`` — typealias for the already-public
    ``JSONValue`` (NOT a rename — preserves source-compat with existing
    ``identify(traits:)`` / ``track(properties:)`` callers).
  * ``IdentitySnapshot`` — ``(anonymousId, externalId, snapshotAt)``.
    Built from ``PyrxStorage`` reads immediately before/after each
    identity mutation. Both ``before`` and ``after`` are non-Optional
    on ``.identityChanged`` (anonymous-user is a state).

## Implementation

  * ``Sources/PYRXSynapse/Observer/`` — 7 new files for the public
    types + the actor-isolated registry.
  * ``Sources/PYRXSynapse/Pyrx+Observer.swift`` — extension exposing
    the two public observer methods.
  * Fire-points wired (additive):
      - ``PushHandlers.recordPushReceived`` → ``.pushReceived``
      - ``PushHandlers.emitOpened`` / ``emitClicked`` → ``.pushClicked``
      - ``PushHandlers.recordColdStartOpen`` → ``.pushReceivedColdStart``
      - ``IdentityManager.identify`` / ``alias`` / ``logout`` →
        ``.identityChanged(before:after:)``
      - ``EventQueue.drainLoop`` → ``.queueDrained(count:)`` via injected
        ``onDrainComplete`` callback at construction time
  * ``parseAlert`` helper in ``PushHandlers.swift`` extracts
    ``(title, body)`` from ``userInfo["aps"]["alert"]``, supporting
    both string and dict alert shapes per APNs.
  * **Cold-start dedup** (non-negotiable invariant): when iOS delivers
    a notification tap via BOTH ``launchOptions[.remoteNotification]``
    AND ``userNotificationCenter(_:didReceive:)``, the publisher
    dedups by ``push_log_id`` within a 5s window so
    ``.pushReceivedColdStart`` fires exactly once AND ``.pushClicked``
    does NOT fire for the cold-start payload. ``ColdStartDedupTests``
    proves this.
  * **Replay buffer of 4** — late subscribers receive the most-recent
    4 events. Documented contract.

## Test coverage

  * 30+ new XCTest cases in ``Tests/PYRXSynapseTests/Observer/`` across
    5 files: ``ObserverRegistryTests``, ``EventStreamTests``,
    ``FirePointsTests``, ``IdentityEventsTests``, ``ColdStartDedupTests``.
  * Full suite: **216/216 pass** (no regression — verify via
    ``swift test``).

## Sample app

  * New 6th tab in ``Examples/SwiftUIDemo/SwiftUIDemo/`` — ``Observer``
    tab demonstrates ``Pyrx.shared.events()`` via SwiftUI's ``.task``
    modifier, shows received events as a scrollable list.

## Docs

  * ``docs/observers.md`` — API reference + lifecycle + cold-start
    dedup + replay buffer + forward-compat + error semantics.
  * ``README.md`` — "What's new in 0.1.2" callout + new entry in the
    Documentation table + observers bullet in Features.

## Forward-compatibility

``PyrxEvent`` is non-``@frozen`` for source consumers (SPM, CocoaPods).
Adding cases in future minor versions is source-compatible — existing
``switch`` statements continue to compile, with a Swift warning to
handle the new case.

Library evolution (``-enable-library-evolution`` / ``BUILD_LIBRARY_FOR_DISTRIBUTION``)
is NOT enabled in 0.1.2 — deferred to whenever we first ship binary
xcframework distribution (or 1.0). Until then, the resilience contract
only applies to source consumers; binary consumers (none today) would
treat the enum as ``@frozen``. Documented in ``docs/observers.md``.

## Cross-repo state

This PR is part of a coordinated Phase 9.2.1 release:

  * PR-1 (this PR): iOS observer API → publishes as PYRXSynapse 0.1.2
  * PR-2: Android observer SharedFlow → publishes as
    ``tech.pyrx.synapse:synapse-{core,push}:0.1.4``
    (in flight in parallel, ``pyrx-synapse-android`` repo)
  * PR-3: RN bridge wiring → publishes as
    ``@pyrx/synapse-react-native@0.2.0`` (opens as DRAFT against
    ``link:``-resolved local SPM/Maven paths once PR-1 + PR-2 review)
  * PR-4: monorepo banner / ADR-0005 / docs cross-links

Reviewable independently of PR-2 (different review surface). Merge +
publish to SPM + CocoaPods Trunk before PR-3 flips from DRAFT to READY.

## Version

``PYRXSynapse 0.1.1 → 0.1.2`` via ``scripts/bump-version.sh 0.1.2``
(podspec + ``PyrxConstants.sdkVersion`` + hardcoded test assertion in
``PushRegistrationTests.swift`` all in lockstep). Bump-script confirmed
``swift test`` clean post-bump (216/216).

## Resolutions applied from preflight

The previous agent's ``feature-dev:code-explorer`` pass surfaced 8
plan-vs-reality conflicts. Resolutions (full rationale in the upstream
``pyrx.synapse`` PR thread):

  1. ``IdentitySnapshot`` shape: ``(anonymousId, externalId, snapshotAt)``
     — NOT ``IdentityResult`` (which is an RPC receipt, not a snapshot).
  2. ``before``/``after`` non-Optional on ``.identityChanged`` (anonymous
     user is a state).
  3. ``PyrxAttributeValue`` is a ``typealias JSONValue`` (NOT a rename —
     ``JSONValue`` already public since 0.1.0).
  4. Library evolution deferred — see Forward-compatibility above.
  5. Test layout: ``Tests/PYRXSynapseTests/Observer/`` subfolder.
  6. Sample app at ``Examples/SwiftUIDemo/SwiftUIDemo/ObserverDemoView.swift``
     (flat, matches convention); 6th tab via ``ContentView.swift``.
  7. ``queueDrained`` plumbing: ``onDrainComplete`` callback injected
     at ``EventQueue.init`` time, default ``{ _ in }`` for backward
     compat.
  8. APNs ``aps.alert`` parser added for ``PushReceivedEvent.title`` +
     ``body`` (supports both string and dict shapes).

Co-authored-by: trieutruong <trieutruong.dev@gmail.com>
Assets 2
Loading