Skip to content

Prototype B: raw async/await launch — no LifecycleKit at all#118

Open
kyleve wants to merge 2 commits into
typed-lifecycle-vanillafrom
raw-async-lifecycle
Open

Prototype B: raw async/await launch — no LifecycleKit at all#118
kyleve wants to merge 2 commits into
typed-lifecycle-vanillafrom
raw-async-lifecycle

Conversation

@kyleve

@kyleve kyleve commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Problem

This is a prototype for comparison, not a default-merge candidate — the last rung of the ladder: shipped combinators (#116) → context-driven functions (#117, the base of this PR) → no kit at all. The question it answers: what does the app look like when launch is nothing but raw async/await, and what is the engine actually earning?

Changes

Both kit targets are deleted (LifecycleKit, LifecycleKitUI, their test bundles and scheme entries; the host smoke tests move to StuffCoreTests). Launch is one raw async task per attempt:

// WhereLaunch.run — the whole engine is now this function's shape
let services = try await openServicesIfNeeded()     // still the one store open
let session  = model.startSession(services: services)
await onServicesReady(session.services)
await configureSessionIfHeadless()                  // service the wake NOW, then…
try await state.whenSceneActive()                   // ← promotion, as a PARK
if !model.hasOnboarded { try await onboarding(handle, session) }
state.publish(.ready(session))
  • Promotion is a park, not a re-drive. The task awaits scene activation before its foreground tail, so a headless wake is serviced above the park and simply never proceeds. This dissolves whole categories of machinery: no LifecycleReason/buildsNoViewTree, no run-once memoization (nothing ever runs twice), no re-drive semantics. The one cost: an ordering the memo used to handle uniformly must now be branched explicitly (session configuration runs before the park when headless, after the onboarding park when the scene is already active — so a reset can't resume GPS into a freshly-erased store mid-re-onboarding).
  • App-owned remnants in WhereUI (~230 lines total): WhereLaunchState (one Phase enum — launching | onboarding(handle, session) | failed | ready(session) — plus the scene signal and attempt/fan task ownership) and OnboardingHandle (the cancellation-aware, stale-safe resolution token). RootView absorbs the container's surface switching with the same zoom-reveal.
  • No retry, by design (the discussion that motivated this prototype): the store-race that retry once caught was fixed structurally by injection in Fix fresh-install launch failure: canonical store, prompt promotion, honest splash #99/Typed LifecycleKit: compile-checked launch plans, engine/UI split, Where migration #116; launch failure is now terminal — logged, .failed, no button — and the recovery is relaunching the app. Reset keeps its safety semantics without retry: cancel + drain the attempt and its fan, erase, then a fresh attempt; a failed erase parks terminal with preferences intact.
  • DetachedFailureReporter is deleted: the fan bodies are non-throwing session methods, so there is no failure channel left to mirror.

The three-way trade

Combinators (#116) Context functions (#117) Raw async (this PR)
Swift LOC vs. previous rung baseline −412 −3,000 (net, incl. tests)
Ordering / use-before-produce combinator constraints plain data flow plain data flow
Run-once across promotion engine memo engine memo structural (park; nothing re-runs)
Retry resume-at-node re-run + memo (free) none (terminal failure)
Invariants held by engine, enforced engine + one convention conventions WhereLaunch documents (one store open, effects-vs-park placement, no phase writes from a cancelled attempt)
Reusability any app any app Where-only (nothing left to reuse)
Engine test depth 320-seed fuzz + engine suites same, ported gone — Where-level behavior tests only

The honest summary: B is the leanest by far and the park model is genuinely elegant for this app's shape — but every guarantee became a convention, the seeded fuzz coverage died with the engine, and a second app (or a second gate, or resurrected retry) starts re-growing the kit by hand.

Testing

Where-level behavior tests with the scripted scene signal: a headless wake services tracking without passing the park (no capture, no .ready), activation resumes the tail, onboarding parks/resolves and its failure is terminal, onServicesReady re-fires on the reset relaunch, reset returns to onboarding over an empty store with preferences restored, and a reset while parked on onboarding drains the old attempt (its stale handle is a no-op). One accepted coverage loss vs. the base branch, named here: the reset-erase failure path (previously driven through the engine's failure-injection seam) is no longer unit-drivable — reset's catch is exercised only by reading. Full Stuff-iOS-Tests scheme green; ./swiftformat --lint clean.

kyleve added 2 commits July 22, 2026 14:20
Plan steps: raw-strip + raw-launch (compile-coupled: deleting the kit
targets breaks every consumer in the same build), tests included so the
commit is green.

- Both kit targets (LifecycleKit, LifecycleKitUI, their test bundles,
  schemes, and Stuff-iOS-Tests entries) are deleted from Package.swift/
  Project.swift. The host smoke tests (ShowLifecycleTests,
  StuffTestHostSmokeTests) move to StuffCoreTests.
- WhereLaunch.run is one raw async task per attempt, started by
  WhereLaunch.start from the app delegate. Promotion is a *park*, not a
  re-drive: the task awaits scene activation before its foreground tail
  (onboarding, the one-shot fix), so a headless wake is serviced above
  the park and simply never proceeds — no launch-reason enum, no
  buildsNoViewTree rule, no memoization (nothing ever runs twice;
  idempotence is the same state checks the model already has). The one
  ordering the park model must branch on explicitly: session
  configuration runs before the park for a headless-so-far attempt, but
  after the onboarding park when the scene is already active, so a
  reset can't resume GPS into the freshly-erased store while the user
  re-onboards.
- App-owned remnants in WhereUI, per the modeling-state conventions:
  WhereLaunchState (one Phase enum — launching | onboarding(handle,
  session) | failed | ready(session) — plus the scene-activation signal
  and attempt/fan task ownership) and OnboardingHandle (the resolution
  token, cancellation-aware, stale-handle-safe).
- Failure is terminal (no retry): launch failures log + park in
  .failed; the new LaunchFailureView offers no button — the recovery is
  relaunching the app. WhereLaunch.reset cancels and drains the
  in-flight attempt and its fan, erases, clears preferences, then
  begins a fresh attempt; a cancelled attempt never writes the phase.
  DetachedFailureReporter is deleted (the fan bodies are non-throwing
  session methods; there is no failure channel to mirror).
- RootView absorbs the container: no view tree until a scene has been
  active, then a switch over the phase animated on surface identity
  with the same zoom-reveal; SettingsView drives the reset through the
  launch state in the environment.
- Tests are Where-level behavior tests: headless wake serviced without
  passing the park, activation resumes the tail, onboarding
  parks/resolves (+ terminal failure via handle.fail), the
  onServicesReady hook re-fires on the reset relaunch, reset returns to
  onboarding over an empty store, and a reset while parked on
  onboarding drains the old attempt (its stale handle is a no-op). The
  kit's engine/fuzz suites die with the engine — an accepted loss
  recorded in the PR body.
Plan step: raw-tests-pr (docs half).

Where/AGENTS and WhereUI README describe the raw launch (the park, the
terminal failure, the conventions WhereLaunch now owns); the root
AGENTS/README drop the kit from the target catalog and the double-link
note; StuffTestHost/TestHostSupport docs point at the smoke tests' new
home in StuffCoreTests. ./sync-agents run.
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