AXIAM PHP SDK

RefreshGuard
in package

FinalYes

Shared-promise clear-on-both-paths helper (CONTRACT.md §9, D-06).

self::settle() wraps a refresh call's raw PromiseInterface so that, regardless of success or failure, the caller's $onClear closure runs exactly once — clearing the caller's own stored promise slot (e.g. Session::$refreshPromise) so the NEXT 401 always starts a brand-new refresh attempt (§9.3: no retry loop; a failed refresh is never cached for the next caller, mirroring the C# sibling Axiam.Sdk.Auth.RefreshGuard's "never cache a faulted refresh" invariant).

PHP has no cross-object mutable-reference primitive that would let this helper safely OWN a mutable promise-slot on behalf of multiple unrelated session objects (unlike C#'s field-holding RefreshGuard class), so the slot itself stays on the owning session (Axiam\Sdk\Session today; a future gRPC session would hold its own field the same way). What this class DOES factor out — and what every such session must apply identically — is the clear-on-both-paths bookkeeping and the normalize-to-AuthError failure translation, so REST and (later) gRPC never drift on that one piece of behavior (D-06's "ONE mechanism" requirement).

Table of Contents

Methods

join()  : mixed
Wait for an ALREADY-IN-FLIGHT shared refresh to settle and return its outcome — the waiter's half of CONTRACT.md §9 rule 2 (F-06).
settle()  : PromiseInterface

Methods

join()

Wait for an ALREADY-IN-FLIGHT shared refresh to settle and return its outcome — the waiter's half of CONTRACT.md §9 rule 2 (F-06).

public static join(PromiseInterface $shared) : mixed

A caller that finds the guard occupied MUST end up holding the leader's outcome: AXIAM refresh tokens are opaque, server-stored and single-use with rotation, so a second wire call would replay an already-consumed token. The obvious way to wait — $shared->wait() — is exactly what must NOT be done here, and is why this method exists:

GuzzleHttp\Promise\Promise::wait() does not "wait" in the concurrent sense. It drives the promise: it takes the underlying promise's wait function, NULLS it, and runs it. The leader already did that when it started its own wire call, so a second wait() from another fiber/coroutine walks the same wait list, finds a pending promise with no wait function left, and rejects the leader's promise with "Cannot wait on a promise that has no internal wait function". That kills the in-flight refresh for everyone AND frees the guard slot mid-flight, so the next caller starts a second POST /oauth2/token with a refresh token the leader has already spent — the precise failure §9 rule 2 forbids.

So this observes rather than drives: it registers a callback on the shared promise, drains Guzzle's task queue (which is all that is needed in the overwhelmingly common case — a promise that has already settled and merely has deferred callbacks outstanding), and otherwise yields to whatever scheduler is driving the leader until the outcome arrives. Rule 6(a)/(d) fall out of this by construction: the callback is registered BEFORE any yielding, so the outcome cannot be missed no matter when the leader clears the slot, and a caller that arrives after full settlement never gets here — it acquires the free guard and refreshes afresh.

Parameters
$shared : PromiseInterface

The in-flight refresh promise returned by Session::refreshGuard()'s promise key. Already normalized by self::settle(), so its failure mode is AuthError.

Tags
throws
AuthError

The leader's failure, shared verbatim with every waiter (§9 rule 2: "on failure, all waiting requests fail with AuthError"), or a fresh one if the bounded wait is exhausted (§9 rule 5).

Return values
mixed

The leader's fulfilment value — for oidc_refresh, the decoded TokenResponse array of the ONE wire call every caller shares.

settle()

public static settle(PromiseInterface $refreshCall, callable(): void $onClear[, callable(mixed): mixed|null $onSuccess = null ]) : PromiseInterface
Parameters
$refreshCall : PromiseInterface

The raw in-flight refresh request promise.

$onClear : callable(): void

Clears the caller's stored promise slot. Invoked exactly once, on EITHER the success or the failure path — never on both, never on neither.

$onSuccess : callable(mixed): mixed|null = null

Optional success-path transform (e.g. CSRF-token capture) run AFTER $onClear, before the settled value is handed back to the caller.

Return values
PromiseInterface
On this page

Search results