// Copyright IBM Corp. 2014, 2026 // SPDX-License-Identifier: BUSL-1.1 package promising import ( "context" "sync" ) // Once is a higher-level wrapper around promises that is similar to // the Go standard library's sync.Once but executes the one-time function // in an asynchronous task and makes all subsequent calls block until the // asynchronous task has completed, after which all calls will return the // result of the initial call. type Once[T any] struct { get PromiseGet[T] promiseID PromiseID mu sync.Mutex } // Do makes an asynchronous call to function f only on the first call to // this method. // // The first and all subsequent calls then block on the completion of that // asynchronous call and all return its single result. // // The context used for the call must belong to an active task. The context // passed to f will belong to a separate asynchronous task and so f can // create its own promises and create further asynchronous tasks to deal // with them, as normal. // // The typical way to use Do is to have only a single callsite where f is // set to a function literal whose behavior and results would be equivalent // regardless of which instance of its closure happens to be the chosen on // to actually run. All subsequent calls will ignore f entirely, so it is // incorrect (and useless) to try to vary the effect of f between calls. // // This function will return an error of type [ErrSelfDependent] if two // different Once instances attempt to mutually depend on one another for // completion. This means that, unlike standard library sync.Once, // self-dependence cannot cause a deadlock. (Other non-promise-related // synchronization between calls can still potentially deadlock, though.) // // If f panics then that prevents the internal promise from being resolved, // and so all calls to Do will return [ErrUnresolved]. However, there is // no built-in facility to catch and recover from such panics since they occur // in a separate goroutine from all of the waiters. func (o *Once[T]) Do(ctx context.Context, name string, f func(ctx context.Context) (T, error)) (T, error) { AssertContextInTask(ctx) o.mu.Lock() if o.get == nil { // We seem to be the first call, so we'll get the asynchronous task // running and then block on its result. resolver, get := NewPromise[T](ctx, name) o.get = get o.promiseID = resolver.PromiseID() o.mu.Unlock() // The responsibility for resolving the promise transfers to the // asynchronous task, which makes it valid for this main task to // await it without a self-dependency error. AsyncTask( ctx, resolver, func(ctx context.Context, resolver PromiseResolver[T]) { v, err := f(ctx) resolver.Resolve(ctx, v, err) }, ) } else { o.mu.Unlock() } // Regardless of whether we launched the async task or not, we'll // wait for it to resolve the promise before we return. return o.get(ctx) } // PromiseID returns the unique identifier for the backing promise of the // receiver, or [NoPromise] if the once hasn't been started yet. // // If PromiseID returns [NoPromise] then that result might be immediately // invalidated by a concurrent or subsequent call to [Once.Do]. However, // if PromiseID returns a nonzero promise ID then it's guaranteed to remain // consistent for the remaining lifetime of the object. func (o *Once[T]) PromiseID() PromiseID { o.mu.Lock() ret := o.promiseID o.mu.Unlock() return ret }