feat: add AccessContext types (replacement for #6413)#6549
feat: add AccessContext types (replacement for #6413)#6549jamesopstad wants to merge 4 commits intomainfrom
Conversation
|
LGTM |
|
The generated output of Full Type Diffdiff -r types/generated-snapshot/latest/index.d.ts bazel-bin/types/definitions/latest/index.d.ts
12726,12859d12725
< type EvaluationContext = Record<string, string | number | boolean>;
< interface EvaluationDetails<T> {
< flagKey: string;
< value: T;
< variant?: string | undefined;
< reason?: string | undefined;
< errorCode?: string | undefined;
< errorMessage?: string | undefined;
< }
< interface FlagEvaluationError extends Error {}
< /**
< * Feature flags binding for evaluating feature flags from a Cloudflare Workers script.
< *
< * @example
< * ```typescript
< * // Get a boolean flag value with a default
< * const enabled = await env.FLAGS.getBooleanValue('my-feature', false);
< *
< * // Get a flag value with evaluation context for targeting
< * const variant = await env.FLAGS.getStringValue('experiment', 'control', {
< * userId: 'user-123',
< * country: 'US',
< * });
< *
< * // Get full evaluation details including variant and reason
< * const details = await env.FLAGS.getBooleanDetails('my-feature', false);
< * console.log(details.variant, details.reason);
< * ```
< */
< declare abstract class Flags {
< /**
< * Get a flag value without type checking.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Optional default value returned when evaluation fails.
< * @param context Optional evaluation context for targeting rules.
< */
< get(
< flagKey: string,
< defaultValue?: unknown,
< context?: EvaluationContext,
< ): Promise<unknown>;
< /**
< * Get a boolean flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getBooleanValue(
< flagKey: string,
< defaultValue: boolean,
< context?: EvaluationContext,
< ): Promise<boolean>;
< /**
< * Get a string flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getStringValue(
< flagKey: string,
< defaultValue: string,
< context?: EvaluationContext,
< ): Promise<string>;
< /**
< * Get a number flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getNumberValue(
< flagKey: string,
< defaultValue: number,
< context?: EvaluationContext,
< ): Promise<number>;
< /**
< * Get an object flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getObjectValue<T extends object>(
< flagKey: string,
< defaultValue: T,
< context?: EvaluationContext,
< ): Promise<T>;
< /**
< * Get a boolean flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getBooleanDetails(
< flagKey: string,
< defaultValue: boolean,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<boolean>>;
< /**
< * Get a string flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getStringDetails(
< flagKey: string,
< defaultValue: string,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<string>>;
< /**
< * Get a number flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getNumberDetails(
< flagKey: string,
< defaultValue: number,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<number>>;
< /**
< * Get an object flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getObjectDetails<T extends object>(
< flagKey: string,
< defaultValue: T,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<T>>;
< }
< /**
< * Evaluation context for targeting rules.
< * Keys are attribute names (e.g. "userId", "country"), values are the attribute values.
< */
diff -r types/generated-snapshot/latest/index.ts bazel-bin/types/definitions/latest/index.ts
12743,12876d12742
< export type EvaluationContext = Record<string, string | number | boolean>;
< export interface EvaluationDetails<T> {
< flagKey: string;
< value: T;
< variant?: string | undefined;
< reason?: string | undefined;
< errorCode?: string | undefined;
< errorMessage?: string | undefined;
< }
< export interface FlagEvaluationError extends Error {}
< /**
< * Feature flags binding for evaluating feature flags from a Cloudflare Workers script.
< *
< * @example
< * ```typescript
< * // Get a boolean flag value with a default
< * const enabled = await env.FLAGS.getBooleanValue('my-feature', false);
< *
< * // Get a flag value with evaluation context for targeting
< * const variant = await env.FLAGS.getStringValue('experiment', 'control', {
< * userId: 'user-123',
< * country: 'US',
< * });
< *
< * // Get full evaluation details including variant and reason
< * const details = await env.FLAGS.getBooleanDetails('my-feature', false);
< * console.log(details.variant, details.reason);
< * ```
< */
< export declare abstract class Flags {
< /**
< * Get a flag value without type checking.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Optional default value returned when evaluation fails.
< * @param context Optional evaluation context for targeting rules.
< */
< get(
< flagKey: string,
< defaultValue?: unknown,
< context?: EvaluationContext,
< ): Promise<unknown>;
< /**
< * Get a boolean flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getBooleanValue(
< flagKey: string,
< defaultValue: boolean,
< context?: EvaluationContext,
< ): Promise<boolean>;
< /**
< * Get a string flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getStringValue(
< flagKey: string,
< defaultValue: string,
< context?: EvaluationContext,
< ): Promise<string>;
< /**
< * Get a number flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getNumberValue(
< flagKey: string,
< defaultValue: number,
< context?: EvaluationContext,
< ): Promise<number>;
< /**
< * Get an object flag value.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getObjectValue<T extends object>(
< flagKey: string,
< defaultValue: T,
< context?: EvaluationContext,
< ): Promise<T>;
< /**
< * Get a boolean flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getBooleanDetails(
< flagKey: string,
< defaultValue: boolean,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<boolean>>;
< /**
< * Get a string flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getStringDetails(
< flagKey: string,
< defaultValue: string,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<string>>;
< /**
< * Get a number flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getNumberDetails(
< flagKey: string,
< defaultValue: number,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<number>>;
< /**
< * Get an object flag value with full evaluation details.
< * @param flagKey The key of the flag to evaluate.
< * @param defaultValue Default value returned when evaluation fails or the flag type does not match.
< * @param context Optional evaluation context for targeting rules.
< */
< getObjectDetails<T extends object>(
< flagKey: string,
< defaultValue: T,
< context?: EvaluationContext,
< ): Promise<EvaluationDetails<T>>;
< }
< /**
< * Evaluation context for targeting rules.
< * Keys are attribute names (e.g. "userId", "country"), values are the attribute values.
< */ |
| modules = [ | ||
| (name = "worker", esModule = embed "ctx-access-test.js") | ||
| ], | ||
| compatibilityFlags = ["nodejs_compat", "experimental", "enable_ctx_access"], |
There was a problem hiding this comment.
Is nodejs_compat actually needed?
There was a problem hiding this comment.
Correct me if I'm wrong, but yes since the test imports node:assert
efb8315 to
a97d168
Compare
| # When enabled, creating a Blob with a resizable ArrayBuffer will throw a TypeError, matching | ||
| # expected spec behavior. | ||
|
|
||
| enableCtxAccess @174 :Bool |
There was a problem hiding this comment.
Do we really need a compat flag?
Seems to me like we wouldn't have to?
If this is just a temporary measure then just use getWorkerdExperimental() instead
| return js.undefined(); | ||
| } | ||
|
|
||
| // Called by the host runtime (edgeworker) to set the Access context for this request. |
There was a problem hiding this comment.
| // Called by the host runtime (edgeworker) to set the Access context for this request. | |
| // Called by the runtime to set the Access context for this request. |
| // Called by the host runtime (edgeworker) to set the Access context for this request. | ||
| // Must be called before the worker's handler is invoked. | ||
| // | ||
| // Unlike other ExecutionContext fields (props, version, exports) which are injected through the |
There was a problem hiding this comment.
I actually don't think this is right, props is also per request and not per worker.
We should be able to mimic props and do it on the constructor.
That should be the correct place for it.
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
Signed-off-by: Matt Provost <mprovost@cloudflare.com>
a97d168 to
d6ef597
Compare
Merging this PR will degrade performance by 28.78%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | simpleStringBody[Response] |
19.3 µs | 27.1 µs | -28.78% |
Comparing add-access-types (d6ef597) with main (556d2f6)
Footnotes
-
129 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
| } | ||
| }; | ||
|
|
||
| // Base class for the ctx.access object providing Cloudflare Access authentication context. |
There was a problem hiding this comment.
Hmm, I see you are following the pattern of CacheContext, above. CacheContext is a relatively new class (2 weeks old) which I've never seen before now. Unfortunately, I don't think CacheContext is designed properly. CacheContext seems to have invented a new way of exposing a per-context API, rather than following the common pattern.
It appears that:
CacheContextis declared as an API type with a virtual implementation.- There's a method on
Worker::Apiwhich creates instances of it. - In the production codebase, this method is implemented by calling a method on the production codebase's version of
IoChannelFactory.
Instead, a bunch better approach would have been:
CacheContextshould be a fully concrete class here.- workerd's version of
IoChannelFactoryshould be extended with a new method to get the underlying I/O object needed byCacheContext. - This object may be implemented differently in workerd and production -- that's fine. But the API wrapper
CacheContextis just a wrapper around this I/O object, and is implemented entirely in workerd.
AccessContext should be the same.
However, there's actually a further catch here: IoChannelFactory generally doesn't contain things that are specific to one request. Indeed, in workerd, there is only one IoChannelFactory per worker, not per-request. In the production codebase, there actually is an IoChannelFactory per-request, which is why CacheContext is able to pull what it needs off of that object. However, that's a design flaw in the production codebase which I want to fix.
For information that changes on a per-request basis, the correct thing to do is not to put it on IoChannelFactory at all, but rather to have it passed in as a parameter to newWorkerEntrypoint(), where it can then make its way onto the IoContext.
cc @tewaro who seems to have written the CacheContext stuff.
Replacement for #6413 so that the internal build workflow can run.