Skip to content

Lifecycle Threading and Cancellation

Alessandro Morvillo edited this page Aug 15, 2026 · 1 revision

Lifecycle, Threading, and Cancellation

The native manager has a deterministic lifecycle and explicit concurrency rules. Understanding them prevents duplicate requests, callback races, and UI-thread mistakes.

Lifecycle states

The manager moves through:

uninitialized -> initializing -> initialized -> shutting down -> shut down
  • Initialization is idempotent. Concurrent callers share the active transition and do not create multiple listeners.
  • Shutdown is idempotent. Concurrent callers complete only after the listener actually stops.
  • Shutdown racing with initialization waits until the listener task is installed and then cancels it safely.
  • A shut-down manager cannot be initialized again.
  • Product, entitlement, unfinished, sync, purchase, and finish operations require successful initialization.
  • Operations already active when shutdown starts are allowed to reach their terminal completion.
  • Transaction updates stop being accepted as soon as shutdown begins.

Use a new client or manager instance if StoreKit must be initialized again after completed shutdown.

Operation concurrency

Only one request can be active within each category. A duplicate call fails with a stable in-progress error. Different categories may overlap, except when a native safety rule prohibits it.

Unfinished-transaction refresh and transaction finishing are mutually exclusive. This prevents enumeration from restoring a transaction that has just been finished.

The managed facade reserves one completion source per operation category. After cancellation, that reservation remains until the native terminal callback arrives; a new same-category request cannot steal a late callback.

Callback and continuation threads

The wrapper does not dispatch native callbacks to the main thread. TransactionUpdated is also raised on the native callback thread. Managed task continuations use RunContinuationsAsynchronously, but this does not imply the UIKit main thread.

Dispatch UI work explicitly:

private void OnTransactionUpdated(object? sender,
                                  StoreKitTransactionUpdatedEventArgs eventArgs)
{
    MainThread.BeginInvokeOnMainThread(() =>
    {
        this.RefreshPurchaseStatus(eventArgs.Transaction);
    });
}

The example uses MainThread from .NET MAUI Essentials. UIKit-only applications can use their preferred main-thread dispatcher.

Cancellation model

Every IStoreKitClient method accepts a CancellationToken:

  1. A pre-cancelled token prevents native invocation.
  2. Cancellation after invocation stops the caller's managed wait immediately.
  3. The client calls the matching native cancellation selector.
  4. The native Swift task observes cancellation cooperatively.
  5. The original callback eventually drains the reserved operation slot.

Initialization cancellation can restore the manager to the uninitialized state. Refresh cancellation leaves invalidated caches empty, subject to preservation of verified updates received concurrently. Transaction finishing releases its reservation when cancelled before Transaction.finish() starts.

Irreversible races

Cancellation is best effort. A terminal StoreKit result can win the race, and cancellation cannot roll back:

  • App Store UI already presented;
  • a transaction already created;
  • a finish operation already accepted by StoreKit.

After cancelling a purchase wait, continue handling listener events and unfinished transactions. After cancelling a finish wait, reconcile rather than assuming that the transaction remains unfinished.

Disposal

Disposal is not a substitute for graceful shutdown. When possible:

  1. stop initiating new operations;
  2. await ShutdownAsync;
  3. unsubscribe application event handlers;
  4. dispose the client.

Disposal faults pending managed operations. Native work already retained by Swift can complete briefly; the private logging bridge is therefore not force-disposed while it might still receive a final diagnostic event.

Clone this wiki locally