fix: make initialize() resolve only when native manager is ready - #55
Conversation
Android was resolving on service bind even if Flic2Manager was null and never emitted restored. iOS resolved before managerDidRestoreState, so scan could still hit NOT_RESTORED. Await initialize() now means APIs are safe to call. Co-authored-by: Cursor <cursoragent@cursor.com>
There was a problem hiding this comment.
Pull request overview
This PR changes the semantics of initialize() across JS, iOS, and Android so that await initialize() only resolves when the native manager is actually ready for subsequent API calls, reducing initialization/readiness races between platforms.
Changes:
- JS:
initialize()becomes idempotent after successful initialization (no longer throws when already initialized) and documentation is updated to reflect the stronger readiness contract. - iOS:
initialize()defers resolution untilmanagerDidRestoreState/PoweredOn, and removes the redundantNOT_RESTOREDscan gate. - Android:
initialize()resolves only when the service is bound andgetManager()is non-null, and emits a"restored"managerStateChangeevent after readiness.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.
| File | Description |
|---|---|
src/index.ts |
Updates JS initialize() behavior + docs to align with native readiness guarantees. |
ios/Flic2.mm |
Defers init resolution until manager readiness and aligns scan gating with new contract. |
ios/Flic2.h |
Adds storage for pending initialize promise callbacks while waiting for restore/power-on. |
android/src/main/java/nl/xguard/flic2/Flic2Module.kt |
Tightens Android init readiness to require a non-null manager and emits a restored event. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Dedupe in-flight JS initialize, reject Android re-entry while pending, unbind after null-manager failure so retries work, and track process-level iOS restore so RN remount still resolves. Co-authored-by: Cursor <cursoragent@cursor.com>
Unbind alone left a running service with a null manager; onCreate would not re-run on rebind. stopService lets the next initialize() start fresh. Co-authored-by: Cursor <cursoragent@cursor.com>
JS Flic2 is a singleton but the native module is not. Remount fast-path now reassigns FLICManager/button delegates. Pending init rejects on invalidate. Android reset uses Handler and invalidate always stopService. Co-authored-by: Cursor <cursoragent@cursor.com>
sharedManager is non-null after configure but before managerDidRestoreState. Keep an explicit NOT_RESTORED gate so scan cannot race await initialize(). Co-authored-by: Cursor <cursoragent@cursor.com>
Sentry analysis: does this PR fix the reported
|
| Field | Value |
|---|---|
| Events | 2 (1 user) |
| Env | staging |
| OS / device | Android 16 / Samsung mid-range |
| Error | Error: Manager not initialized |
| Mechanism | onunhandledrejection |
| Stacktrace | None |
| Screen | Flic “add button” flow (both events) |
| App state | in_foreground: false (both events) |
| BT | scan permission granted; connect permission not granted |
What this PR (#55) fixes
In react-native-flic2@2.0.0-beta.25 (current consumer dependency):
- Android:
initialize()could resolve on service bind even whengetManager()was still null → immediate follow-up APIs rejectNOT_INITIALIZED. - Android: never emitted
managerStateChange/restored. - iOS:
initialize()could resolve before restore → scan/NOT_RESTOREDraces (same class of “await init ≠ ready”).
This PR makes await Flic2.initialize() mean the native manager is actually ready (gate on manager, emit restored, defer iOS resolve until restored, remount/teardown hardening).
That is the correct library-layer fix for the post-successful-init race.
Does #55 alone clear this Sentry issue?
Not by itself — only after consumers bump, and the app may still have a gap.
| Failure mode consistent with Sentry | Fixed by #55? |
|---|---|
initialize() resolved, then getButtons/connect raced before manager ready |
Yes |
Screen calls getButtons before anyone successfully initialize()s |
No — APIs still correctly reject NOT_INITIALIZED |
App backgrounded (in_foreground: false) so Android FGS/initialize fails or never runs, then UI still calls native APIs |
No — out of scope here (app lifecycle / foreground-service policy) |
These events look most like screen call before ready + background, not only a clean “init returned then raced” stack. Shipping #55 without a consumer version bump + thin app gating would not clear this telemetry on its own.
Verdict
| Question | Answer |
|---|---|
Is #55 the right root fix for the library bug behind NOT_INITIALIZED after init? |
Yes |
| Will merging #55 alone stop the observed Sentry events? | No — consumer still on beta.25; UI can call APIs without a successful init; background FGS remains an app concern |
| What closes the loop? | Merge + publish a new beta → bump the consumer → thin app change: don’t call Flic APIs until await Flic2.initialize() succeeds; defer Android init while backgrounded — not app-level NOT_INITIALIZED retry loops |
Bottom line: Merge this PR. Necessary library fix, not sufficient alone for the observed events; consumer needs a small follow-up after the version bump.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 6b1db2b. Configure here.

Summary
initialize()only whengetManager()is non-null after service bind; emitmanagerStateChange{ event: "restored" }so JS readiness matches iOS.initialize()resolve untilmanagerRestored(managerDidRestoreState/ PoweredOn). Keep aNOT_RESTOREDgate onstartScanas belt-and-suspenders for callers that race an in-flightinitialize()(preferred path is stillawait initialize()).await initialize()means APIs are safe; make re-init idempotent (return instead of throw).Fixes the library-level
NOT_INITIALIZED/ readiness race whereawait initialize()could resolve before the native manager was usable.Test plan
await Flic2.initialize()then immediatelygetButtons()/connectAllKnownButtons()/startScan()— noNOT_INITIALIZEDmanagerStateChangewithevent: "restored"after initawait initialize()— scan works; callingstartScanbefore restore still rejectsNOT_RESTOREDFlic2.initialize()returns without throwing