feat(ios): wait for SharedImage plugin before initial startup check (Phase 2A)
Eliminate the iOS startup race between asynchronous SharedImage plugin registration and the first shared-image check. Previously the initial check fired on a fixed 1000ms timer that only assumed the native plugin (registered by AppDelegate with retries from T+500ms) was ready, so a slow registration could make the first getSharedImage() call throw and miss a cold-start share. Replace the fixed delay with waitForSharedImagePluginReady(), which probes the plugin via a read-only hasSharedImage() call and retries within a bounded budget (10 attempts, 300ms apart) until the plugin actually responds. The initial check runs only once readiness is confirmed, with a best-effort fallback if the budget is exhausted. Scope is limited to the initial startup check on iOS. appStateChange, appUrlOpen/handleDeepLink, router navigation, share processing, Android behavior, and all native Swift code are unchanged. Temporary share-target diagnostics are preserved and extended with startup readiness logging. Document the change as a Phase 2A section in doc/share-target-ios-audit.md.
This commit is contained in:
@@ -436,6 +436,81 @@ All removal logic was in `SharedImageUtility.getSharedImageData()`:
|
||||
|
||||
---
|
||||
|
||||
## Deterministic Startup Plugin Readiness
|
||||
|
||||
**Implemented:** 2026-06-26 (Phase 2A)
|
||||
|
||||
Phase 2A removes the iOS startup race between native `SharedImage` plugin
|
||||
registration and the first JS shared-image check. All changes are confined to
|
||||
`src/main.capacitor.ts`; no Swift code changed.
|
||||
|
||||
### Race condition removed
|
||||
|
||||
Previously the initial iOS shared-image check ran on a fixed timer:
|
||||
|
||||
```ts
|
||||
const checkDelays = ... : [1000]; // iOS
|
||||
checkDelays.forEach((delay) => setTimeout(() => checkForSharedImageAndNavigate(), delay));
|
||||
```
|
||||
|
||||
The native `SharedImagePlugin` is registered asynchronously by
|
||||
`AppDelegate.didFinishLaunchingWithOptions` with up to 5 retries starting at
|
||||
T+500ms (`AppDelegate.swift:21–40`). The fixed 1000ms JS delay only *assumed*
|
||||
registration had completed by then. When registration was slow (or the WebView
|
||||
booted unusually fast), the first `checkForSharedImageAndNavigate()` could call
|
||||
`SharedImage.getSharedImage()` before the native plugin existed, the call would
|
||||
throw, and the cold-start share could be missed until a later `appStateChange`.
|
||||
This corresponds to race condition #3 in *Potential Race Conditions* above.
|
||||
|
||||
### How plugin readiness is now determined
|
||||
|
||||
The fixed 1000ms iOS delay is replaced with an explicit, deterministic wait
|
||||
(`waitForSharedImagePluginReady()` in `main.capacitor.ts`):
|
||||
|
||||
- The plugin is probed with a lightweight, read-only `SharedImage.hasSharedImage()`
|
||||
call. A successful resolution proves the native plugin instance is registered
|
||||
and reachable from JS. `hasSharedImage()` does not consume or mutate the pending
|
||||
share (non-destructive since Phase 1C), so probing is side-effect free.
|
||||
- If the probe throws (plugin not yet registered), it retries within a bounded
|
||||
budget: `STARTUP_PLUGIN_MAX_ATTEMPTS = 10` attempts spaced
|
||||
`STARTUP_PLUGIN_RETRY_DELAY_MS = 300` ms apart (~3s ceiling, covering the
|
||||
native registration window). No arbitrary sleep is used to *assume* readiness;
|
||||
the delay is only the inter-retry backoff while polling for actual availability.
|
||||
- The very first `checkForSharedImageAndNavigate()` runs only after the probe
|
||||
succeeds. If the budget is exhausted (should not happen in practice), the check
|
||||
is still attempted once as a best-effort fallback so behavior is never worse
|
||||
than the previous fixed-delay path, and `appStateChange` retries on the next
|
||||
activation.
|
||||
|
||||
### Temporary diagnostics
|
||||
|
||||
The retry sequence emits `[ShareTarget]` console diagnostics, consistent with the
|
||||
existing TEMPORARY SHARE TARGET DIAGNOSTICS convention:
|
||||
|
||||
```
|
||||
[ShareTarget] Startup shared-image check waiting for SharedImage plugin
|
||||
[ShareTarget] SharedImage plugin ready after N attempt(s)
|
||||
[ShareTarget] Startup shared-image check giving up after N attempt(s)
|
||||
```
|
||||
|
||||
### Phase 2A Scope (Intentionally Unchanged)
|
||||
|
||||
The retry/readiness logic applies **only** to the initial startup shared-image
|
||||
check. The following are deliberately untouched:
|
||||
|
||||
- `appStateChange` handling (`CapacitorApp.addListener("appStateChange", ...)`)
|
||||
- `appUrlOpen` handling (`handleDeepLink`, `registerDeepLinkListener`)
|
||||
- Router navigation to `/shared-photo`
|
||||
- Share processing (`checkAndStoreNativeSharedImage`, `storeSharedImageInTempDB`)
|
||||
- Android startup behavior (still `[500, 1500, 3000]` ms multi-delay checks)
|
||||
- All native Swift code, including the `AppDelegate` plugin-registration retry
|
||||
|
||||
Because readiness is now confirmed by an actual plugin response rather than a
|
||||
timer, the startup check no longer depends on registration timing, while every
|
||||
other detection path keeps its previous semantics as redundant backstops.
|
||||
|
||||
---
|
||||
|
||||
## Configuration References
|
||||
|
||||
| Resource | Value |
|
||||
|
||||
Reference in New Issue
Block a user