Async execution can start synchronously

@wg_async starts its generator immediately and continues until an unresolved future is yielded or the generator finishes. Work before that point remains in the current call stack. Calling it from a view constructor does not automatically move expensive work to another frame or thread.

post(func) explicitly introduces a zero-delay client callback. That gives a next-tick boundary; it still does not make the called work concurrent. Use owned workers for blocking I/O, and return results to the game thread with a matching owner-generation check.

Cooperative iteration has a work boundary

distributeLoopOverTicks advances the iterator before checking whether to delay. Its minimum and maximum count yielded items, not elapsed time; yielded values are discarded. An expensive iterator step or work skipped before an iterator yield can still block the calling thread. Without a maximum, the whole iterator can finish synchronously if tick pressure never requests a delay.

distributeLoopOverTicks2 first waits while a tick is pending, then checks elapsed wall-clock time between iterator steps. It cannot preempt one expensive step or distribute preparation performed outside the iterator. Neither helper moves work to a background thread or supplies an account/view freshness check. Keep steps bounded, retain cancellation ownership and recheck context across waits.

These are version-sensitive Python scheduling rules from wg_async in the latest public source, not measured frame-time guarantees.

Client futures are single-consumer continuations

The private future retains one pending continuation. A second pending then replaces the first; after completion the retained result is consumed rather than replayed to arbitrary later observers. Await once, then publish a value through an owned cache/event or separate promises if multiple consumers need it.

forwardAsFuture recognizes the exact client future type; another future-like object can be wrapped as an ordinary ready value. It is not a general awaitable adapter. Unconsumed failures and abandoned promises have destructor diagnostics, but garbage collection is not a dependable completion or cleanup protocol.

Timeout is not underlying cancellation

wg_await(future, timeout) modifies that same future. Expiry resumes the waiting generator with TimeoutError and asks the promise to cancel. The producer decides what cancellation actually stops. Delay timers and synchronization waiters have concrete removal handlers; a callback adapter has no automatic cancellation of the operation it wrapped.

A timed-out request or running worker can therefore finish later. Keep request/context identity and reject stale side effects independently. The inspected future does not cancel an existing timer before replacing its stored timeout ID, so apply one timeout per future unless a reverified implementation provides stronger behavior.

Explicit cancellation normally abandons the continuation instead of injecting a standard cancellation exception. The owner cancelling it must restore busy flags and release subscriptions/resources; do not depend solely on an awaiting generator's catch/finally path. Cancellation follows the currently yielded wait, not a tree of every independent operation previously started.

Callback adaptation has an argument contract

await_callback injects a keyword named callback and invokes the supplied function. Zero callback arguments become an empty tuple, one becomes the value itself, and several become a tuple. Existing positional/keyword callback arguments can conflict; a synchronous producer exception happens before a usable future is returned.

Use the adapter only with verified callback naming and exactly-once completion. A timeout cannot retrofit transport cancellation or prevent the original callback body from running later. Preserve the native helper's payload shape rather than assuming every account/request callback has the same signature.

Scopes and synchronization primitives

OwnerBehaviorLifecycle consequence
AsyncScopeWeakly tracks synchronization objects; destruction is terminal.Retain scope and objects; never register or operate after destruction.
AsyncEventSet state releases all current waiters; later waits complete while still set.Level-triggered state, not a counted pulse history.
AsyncSemaphoreConsumes a count or queues FIFO; release wakes a waiter or adds a count.Use nonnegative counts and balance ownership.
AsyncQueueBuffers values/exceptions FIFO or completes the oldest waiter.Destroying abandons buffered entries and pending waits.

Dropping unresolved promises can produce BrokenPromiseError. Native dialog patterns use scope destruction to turn view disappearance into a handled cancel outcome. That is distinct from an operational failure and from a local timeout.

Native thread pools have separate completion rules

The inspected helpers.threads.ThreadPool starts additional workers on repeated start calls and has no running guard. Its running flag is lifecycle state, not worker-liveness proof. Stop discards queued jobs and sends termination jobs but does not join workers or interrupt an active job.

The pool does not automatically marshal job callbacks to the game thread. Some native jobs invoke callbacks directly; others explicitly use BigWorld.callback. Inspect the job and consumer. A stopped submission can only log and return; a bounded full queue can raise. Submission return alone does not establish accepted work or eventual completion.

A late running job remains possible after stop. Keep stale-result checks separate from pool shutdown. Different similarly named pools require their own inspection. See request-context ownership and callback lifecycle.