All files / src/core/bindings Binding.ts

100% Statements 17/17
100% Branches 12/12
100% Functions 7/7
100% Lines 16/16

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 11828x                                               28x                 388x                               388x                 182x 113x     181x                       40x                     75x   75x 21x     54x                   19x                   134x 134x   133x      
import { MissingBindingError } from '@/exceptions/MissingBindingError'
import type { CloudflareEnv } from '@/types/CloudflareEnv'
 
/**
 * Base class for every Cloudflare binding accessor.
 *
 * Cloudflare exposes bindings on the per-request `E` object rather than on a
 * module-level singleton, so an instance never holds the binding itself — it
 * holds the *name* of the binding and resolves it from the `E` passed to
 * each call. That keeps instances safe to create once at module scope and
 * reuse across requests.
 *
 * The name arrives already resolved: subclasses take their settings from
 * `resolve`, so neither this class nor its callers
 * know whether a value was defaulted or overridden.
 *
 * @typeParam TBinding The Cloudflare binding type this class wraps.
 *
 * @class
 *
 * @author Bayu Dwiyan Satria
 * @version 1.0.0
 * @since 1.0.0
 */
export abstract class Binding<TBinding, E extends CloudflareEnv = CloudflareEnv> {
  /**
   * Produces the binding name, on first read.
   */
  private readonly settle: () => string
 
  /**
   * The name once settled, so it is produced only once.
   */
  private settled: string | null = null
 
  /**
   * Constructs a binding accessor.
   *
   * @remarks
   * The name may arrive as a function, and every subclass passes one. Accessors
   * are created at module scope so a single instance is reused across requests,
   * and module scope runs while a consumer's imports are still being hoisted,
   * before it can call `configure()`. A thunk moves the lookup to first use,
   * which is inside a request.
   *
   * @param binding Binding name as declared in `wrangler.json`, or a function
   * producing it when first needed.
   */
  protected constructor(binding: string | (() => string)) {
    this.settle = typeof binding === 'function' ? binding : () => binding
  }
 
  /**
   * The binding name this instance resolves.
   *
   * @returns The binding name as declared in `wrangler.json`.
   */
  public get name(): string {
    if (this.settled === null) {
      this.settled = this.settle()
    }
 
    return this.settled
  }
 
  /**
   * Checks whether the binding is available on the given environment.
   *
   * Use this to degrade gracefully when a binding is optional.
   *
   * @param env The Worker environment.
   * @returns `true` when the binding is present.
   */
  public isBound(env: E): boolean {
    return this.lookup(env) !== null
  }
 
  /**
   * Resolves the binding, failing fast when it is not configured.
   *
   * @param env The Worker environment.
   * @returns The resolved binding.
   * @throws {@link MissingBindingError} When the binding is absent from the environment.
   */
  public resolve(env: E): TBinding {
    const binding = this.lookup(env)
 
    if (binding === null) {
      throw new MissingBindingError(this.name)
    }
 
    return binding
  }
 
  /**
   * Resolves the binding without throwing.
   *
   * @param env The Worker environment.
   * @returns The resolved binding, or `null` when it is not configured.
   */
  public tryResolve(env: E): TBinding | null {
    return this.lookup(env)
  }
 
  /**
   * Reads the binding off the environment by name.
   *
   * @param env The Worker environment.
   * @returns The binding, or `null` when it is absent.
   */
  private lookup(env: E): TBinding | null {
    const bindings = env as unknown as Record<string, TBinding | undefined>
    const binding = bindings ? bindings[this.name] : undefined
 
    return binding === undefined || binding === null ? null : binding
  }
}