diff --git a/api/src/api/context.ts b/api/src/api/context.ts index 6631dea8f1b..42d06dc9b4e 100644 --- a/api/src/api/context.ts +++ b/api/src/api/context.ts @@ -32,8 +32,11 @@ const NOOP_CONTEXT_MANAGER = new NoopContextManager(); export class ContextAPI { private static _instance?: ContextAPI; - /** Empty private constructor prevents end users from constructing a new instance of the API */ - private constructor() {} + /** + * Empty protected constructor prevents end users from constructing a new instance + * of the API while allowing experimental API to extend it. + */ + protected constructor() {} /** Get the singleton instance of the Context API */ public static getInstance(): ContextAPI { @@ -87,7 +90,7 @@ export class ContextAPI { return this._getContextManager().bind(context, target); } - private _getContextManager(): ContextManager { + protected _getContextManager(): ContextManager { return getGlobal(API_NAME) || NOOP_CONTEXT_MANAGER; } diff --git a/api/src/experimental/api/context.ts b/api/src/experimental/api/context.ts new file mode 100644 index 00000000000..21ecafe5806 --- /dev/null +++ b/api/src/experimental/api/context.ts @@ -0,0 +1,106 @@ +import { ContextAPI } from "../../api/context"; +import { Context } from "../../context/types"; +import { ExperimentalContextManager } from "../context/types"; +import { ExperimentalNoopContextManager } from "../context/ExperimentalNoopContextManager"; + +const NOOP_CONTEXT_MANAGER = new ExperimentalNoopContextManager(); + +export class ExperimentalContextAPI extends ContextAPI { + /** + * @experimental this operation should be considered experimental and may make use of experimental APIs. + * {@link with} should be preferred over `attach`/{@link detach} unless there are strong reasons to use this method. + * + * Make a context active in the current execution. Returns a unique restore + * key which must be used with detach to restore the previous context. + * + * The context will remain the active context for the entire asynchronous + * execution unless another context is made active by calling `attach`, + * {@link with}, or {@link detach}, or if a {@link with} callback ends. + * + * If `attach` is used within a {@link with} callback, the context which was active + * before {@link with} was called will be made the active context when the callback + * ends. + * + * Note that every call to this operation should result in a corresponding call to {@link detach} in the reverse order. + * + * @example Example of using context.attach to make context active in a sibling execution + * + * ```typescript + * function func1() { + * api.context.attach(ctx1) + * } + * + * function func2() { + * api.context.active() // returns ctx1 + * } + * + * func1() // ctx1 is made active within this execution + * func2() // ctx1 is still active + * ``` + * + * @example Example of using context.with to override the context set by context.attach + * + * ```typescript + * function func1() { + * api.context.attach(ctx1) + * } + * + * function func2() { + * api.context.active() // returns ctx2 + * } + * + * func1() + * api.context.active() // returns ctx1 + * api.context.with(ctx2, func2) // run func2 with ctx2 active + * api.context.active() // returns ctx1 + * ``` + * + * @example Example of incorrect use of context.attach inside a context.with callback. This is incorrect because attach is called within a with callback, but there is no corresponding detach within the same callback. + * + * ```typescript + * function foo() { + * api.context.active() // returns ctx1 + * api.context.attach(ctx2) // make ctx2 active + * api.context.active() // returns ctx2 + * } + * + * api.context.active() // returns root context + * api.context.with(ctx1, foo) + * api.context.active() // returns root context + * ``` + * + * @param context context to make active in the current execution + * @returns a restore key + */ + public attach(context: Context): symbol { + return this._getContextManager().attach(context); + } + + /** + * @experimental this operation should be considered experimental and may make use of experimental APIs. + * {@link with} should be preferred over {@link attach}/`detach` unless there are strong reasons to use this method. + * + * Restore the context which was active when attach was called using the restore + * token returned by attach. + * + * @param token the restore token returned by attach + */ + public detach(token: symbol): void { + return this._getContextManager().detach(token); + } + + /** + * @experimental this operation should be considered experimental and may make use of experimental APIs. + * Stable context API should be preferred over experimental unless there are strong reasons to use experimental APIs. + * + * Get experimental context manager. If the currently registered context manager does not have the required + * experimental functions, a noop context manager will be returned. + */ + protected override _getContextManager(): ExperimentalContextManager { + const manager = super._getContextManager() as any; + if (typeof manager["attach"] === "function" && typeof manager["detach"] === "function") { + return manager; + } + return NOOP_CONTEXT_MANAGER; + } +} \ No newline at end of file diff --git a/api/src/experimental/context/ExperimentalNoopContextManager.ts b/api/src/experimental/context/ExperimentalNoopContextManager.ts new file mode 100644 index 00000000000..4e580a90a00 --- /dev/null +++ b/api/src/experimental/context/ExperimentalNoopContextManager.ts @@ -0,0 +1,10 @@ +import { NoopContextManager } from "../../context/NoopContextManager"; +import { ExperimentalContextManager } from "./types"; + +export class ExperimentalNoopContextManager extends NoopContextManager implements ExperimentalContextManager { + attach() { + return Symbol("NOOP Context Symbol"); + } + + detach(_token: symbol) {/* nothing */} +} \ No newline at end of file diff --git a/api/src/experimental/context/types.ts b/api/src/experimental/context/types.ts new file mode 100644 index 00000000000..e234b64c094 --- /dev/null +++ b/api/src/experimental/context/types.ts @@ -0,0 +1,25 @@ +import { Context, ContextManager } from "../../context/types" + +export interface ExperimentalContextManager extends ContextManager { + /** + * @experimental this operation should be considered experimental and may make use of experimental APIs. + * + * Make a context active in the current execution. + * For more info, see {@Link ContextAPI.attach}. + * + * @param context context to make active in the current execution + * @returns a restore key + */ + attach(context: Context): symbol; + + /** + * @experimental this operation should be considered experimental and may make use of experimental APIs. + * + * Restore the context which was active when attach was called using the restore + * token returned by attach. + * For more info, see {@Link ContextAPI.detach}. + * + * @param token the restore token returned by attach + */ + detach(token: symbol): void; +} \ No newline at end of file