Skip to content

feat(SDK-520): replace magic 1000ms timeouts with named, configurable…#882

Merged
jferrao-itrbl merged 4 commits into
masterfrom
feature/SDK-520-replace-hardcoded-magic-timeouts
Jul 9, 2026
Merged

feat(SDK-520): replace magic 1000ms timeouts with named, configurable…#882
jferrao-itrbl merged 4 commits into
masterfrom
feature/SDK-520-replace-hardcoded-magic-timeouts

Conversation

@jferrao-itrbl

@jferrao-itrbl jferrao-itrbl commented Jul 2, 2026

Copy link
Copy Markdown
Collaborator

🔹 JIRA Ticket(s) if any

✏️ Description

Replace hardcoded magic timeouts:

  • Add IterableConfig.androidWakeDelayMs and authCallbackTimeoutMs to expose the two hardcoded timeouts as tunable, documented values.
  • Replace the fixed-window auth callback gate with an event-driven Promise latch; the timer survives only as a safety-net fallback.

… constants

- Add IterableConfig.androidWakeDelayMs and authCallbackTimeoutMs to
  expose the two hardcoded timeouts as tunable, documented values.
- Replace the fixed-window auth callback gate with an event-driven
  Promise latch; the timer survives only as a safety-net fallback.
@jferrao-itrbl jferrao-itrbl requested a review from a team July 2, 2026 14:18
@github-actions

github-actions Bot commented Jul 2, 2026

Copy link
Copy Markdown
Lines Statements Branches Functions
Coverage: 72%
71.92% (579/805) 61.22% (229/374) 67.18% (174/259)

@qltysh

qltysh Bot commented Jul 2, 2026

Copy link
Copy Markdown

Qlty


⚠️ Comments skipped @jferrao-itrbl doesn't have a Qlty seat in Iterable.

Qlty doesn't post analysis or coverage comments for contributors without a seat. An authorized user can grant @jferrao-itrbl a seat from this pull request's page in Qlty.

Comment thread src/core/classes/Iterable.ts
Comment thread src/core/classes/Iterable.ts Outdated
…default

- Replace shared authLatchResolver/pendingAuthResult with a per-invocation
  FIFO queue so overlapping handleAuthCalled invocations no longer drop
  successCallback/failureCallback
- Route native success/failure events to the oldest pending invocation and
  shift the queue synchronously on resolve
- Raise IterableConfig.authCallbackTimeoutMs default 1000 -> 6000 with
  rationale in TSDoc
- Remove dead AUTH_CALLBACK_TIMEOUT_DEFAULT_MS / ANDROID_WAKE_DELAY_DEFAULT_MS
  fallbacks; IterableConfig is the single source of truth
- Update Iterable.test.ts default assertions (1000 -> 6000), rework
  safety-net test to use a custom short timeout
- Add regression test: two back-to-back handleAuthCalled invocations assert
  both successCallbacks fire with no 'No callback received' warning
- Update CHANGELOG default
@jferrao-itrbl jferrao-itrbl requested a review from joaodordio July 6, 2026 15:12
@joaodordio

Copy link
Copy Markdown
Member

Really nice iteration on the latch, the FIFO scoping and the 6s default both look great. One thing I noticed while tracing through the new design that I think is worth catching before this lands:

The FIFO queue only gets drained in the isIterableAuthResponse branch and in the two native listeners. The .catch and the three other .then branches (bare string, null/undefined, unexpected type) push an AuthInvocation but never remove it, so it sits at the head of pendingAuthInvocations as a zombie. Since native emits handleAuthSuccessCalled or handleAuthFailureCalled in all of those paths too (passAlongAuthToken(null)handleAuthTokenSuccess(null)handleAuthFailureCalled on Android, semaphore always emits on iOS, and the .catch path hits the 30s native latch timeout which still returns and emits), the zombie ends up catching that event and buffering it into pendingResult where nothing consumes it.

Concrete single-flight trace for the .catch case, which is probably the most common non-happy path in production:

  1. handleAuthCalled # 1 fires, inv1 pushed. Queue [inv1].
  2. Customer's authHandler throws (network blip, exception in their token fetch, etc). .catch logs. inv1 stays.
  3. Native latch times out at 30s, emits handleAuthFailureCalled (Android returns null token → AUTH_TOKEN_NULL; iOS semaphore timeout).
  4. Listener finds inv1 at head with no resolver, buffers inv1.pendingResult = FAILURE. onJwtError still fires correctly, all good there.
  5. SDK schedules a retry via IterableAuthManager.scheduleAuthTokenRefresh, eventually emits handleAuthCalled # 2.
  6. inv2 pushed. Queue [inv1, inv2].
  7. authHandler # 2 succeeds with an IterableAuthResponse, latch wired on inv2.
  8. Native # 2 succeeds, emits handleAuthSuccessCalled.
  9. Listener routes to queue[0] which is still inv1 → buffers SUCCESS on the zombie.
  10. inv2's latch never sees it, safety-net wins at 6s, "No callback received" logged, successCallback2 silently dropped.

Also a slow memory leak on the .catch path since every rejected authHandler adds one AuthInvocation for the process lifetime.

The fix I'd suggest is dropping the invocation from the queue in each of those four paths, same shape as the defensive splice you already have in race.then, something like:

const removeInvocation = () => {
  const index = pendingAuthInvocations.indexOf(invocation);
  if (index !== -1) {
    pendingAuthInvocations.splice(index, 1);
  }
};

then call removeInvocation() in the string, null/undefined, unexpected-type branches, and in the .catch. There's a tiny caveat that a late native event for a .catched invocation will then misroute to whatever is at the new head (either nothing, or a newer invocation), but that's a pre-existing bridge limitation with no correlation id and the current behavior is strictly worse since it deterministically blocks the next real callback.

Locking this down with a rejection-then-retry test in the same shape as the great overlap regression test you just added would be perfect, something like: fire handleAuthCalled → reject authHandler → fire handleAuthCalled again → resolve with IterableAuthResponse → assert the second successCallback fires.

…bie invocations

- Add removeInvocation() in handleAuthCalled; call it in the string, null/undefined,
  unexpected-type, and .catch branches so rejected/non-IterableAuthResponse auth handlers
  no longer leave a zombie at queue head
- Prevents the next invocation's native success/failure event from routing to a dead
  invocation and dropping its callback
- Add regression test: handleAuthCalled -> reject -> handleAuthCalled -> resolve ->
  native success asserts second successCallback fires with no 'No callback received' warning
@jferrao-itrbl

jferrao-itrbl commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator Author

Thanks for catching this @joaodordio , really good spot.

Added a removeInvocation() helper scoped to each invocation and called it in all four non-happy paths, dropping the invocation from the queue before native's late event arrives. The no-correlation-id bridge limitation (a late native event for a removed invocation may misroute to the new queue head) is noted in a comment. Strictly better than the zombie-at-head block; full overlap hardening with a correlation id would be a separate bridge-contract change.

Added a regression test: handleAuthCalled → reject authHandlerhandleAuthCalled again → resolve with IterableAuthResponse → emit native success → assert the second successCallback fires and no "No callback received" warning is logged.

All tests green and updated rules to make sure similar scenarios are avoided or caught earlier in the future.

@joaodordio joaodordio left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for addressing the issues João!

LGTM!

@jferrao-itrbl jferrao-itrbl merged commit 016561d into master Jul 9, 2026
15 checks passed
@jferrao-itrbl jferrao-itrbl deleted the feature/SDK-520-replace-hardcoded-magic-timeouts branch July 9, 2026 17:07
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.

2 participants