| 1 | import * as Context from "effect/Context"; |
| 2 | import * as Effect from "effect/Effect"; |
| 3 | import type * as Scope from "effect/Scope"; |
| 4 | import type { Simplify } from "effect/Types"; |
| 5 | import { PluginContext, type ConfigHook } from "./PluginContext.ts"; |
| 6 | import type { RuntimeError } from "./RuntimeError.shared.ts"; |
| 7 | import type * as WorkerdConfig from "./workerd/Config.ts"; |
| 8 | import type * as Workerd from "./workerd/Workerd.ts"; |
| 9 | |
| 10 | export type Plugin<Api = never> = WithApi< |
| 11 | PluginConfig & { readonly defer?: Effect.Effect<PluginConfig, RuntimeError> }, |
| 12 | Api |
| 13 | >; |
| 14 | |
| 15 | type WithApi<T, Api> = [Api] extends [never] |
| 16 | ? Simplify<T> |
| 17 | : Simplify<T & { readonly api: Api }>; |
| 18 | |
| 19 | export interface PluginConfig { |
| 20 | readonly services?: Array<WorkerdConfig.Service>; |
| 21 | readonly sockets?: Array<WorkerdConfig.Socket>; |
| 22 | readonly extensions?: Array<WorkerdConfig.Extension>; |
| 23 | readonly middlewares?: Array<Middleware>; |
| 24 | /** |
| 25 | * Worker-level fields merged into the user worker's configuration (e.g. |
| 26 | * `cacheApiOutbound`). Merged in plugin order; `RuntimeWorker.unsafe` still |
| 27 | * takes precedence. |
| 28 | */ |
| 29 | readonly userWorker?: Partial<WorkerdConfig.Worker>; |
| 30 | /** |
| 31 | * Initializes each workerd process, including replacements after a crash. |
| 32 | * Resources and tasks acquired here are released before the next process |
| 33 | * starts. Replay any state held only inside workerd from this hook. |
| 34 | */ |
| 35 | readonly start?: ( |
| 36 | ports: Workerd.WorkerdPorts, |
| 37 | ) => Effect.Effect<void, RuntimeError, Scope.Scope>; |
| 38 | } |
| 39 | |
| 40 | export interface Middleware { |
| 41 | name: string; |
| 42 | worker: WorkerdConfig.Worker; |
| 43 | upstreamBindingName: string; |
| 44 | order?: number; |
| 45 | } |
| 46 | |
| 47 | export type PluginBuilder<Api = never> = |
| 48 | | Plugin<Api> |
| 49 | | Effect.Effect<Plugin<Api>, RuntimeError, PluginContext>; |
| 50 | |
| 51 | export type PluginIdentifier<T extends string = string> = |
| 52 | `cloudflare-runtime/plugin/${T}`; |
| 53 | |
| 54 | /** |
| 55 | * When true, simulator services backed by Durable Objects (KV, R2, ...) |
| 56 | * handle control operations (fake timers, storage inspection) sent via their |
| 57 | * reserved control-op header. |
| 58 | * |
| 59 | * **Only intended for tests** (provide with |
| 60 | * `Layer.succeed(UnsafeEnableControlEndpoints, true)`): anyone able to reach |
| 61 | * such a service can then run arbitrary SQL queries and read arbitrary blobs |
| 62 | * across every namespace/bucket in this runtime. |
| 63 | */ |
| 64 | export const UnsafeEnableControlEndpoints = Context.Reference( |
| 65 | "cloudflare-runtime/plugin/UnsafeEnableControlEndpoints", |
| 66 | { defaultValue: () => false }, |
| 67 | ); |
| 68 | |
| 69 | export type PluginService< |
| 70 | Self, |
| 71 | Identifier extends PluginIdentifier, |
| 72 | Api = never, |
| 73 | > = Context.ServiceClass<Self, Identifier, PluginBuilder<Api>>; |
| 74 | |
| 75 | export const Service = |
| 76 | <Self, Api = never>() => |
| 77 | <Identifier extends PluginIdentifier>( |
| 78 | identifier: Identifier, |
| 79 | ): Context.ServiceClass<Self, Identifier, PluginBuilder<Api>> => |
| 80 | Context.Service<Self, PluginBuilder<Api>>()(identifier); |
| 81 | |
| 82 | export const use = <Self, Identifier extends PluginIdentifier, Api, A, E, R>( |
| 83 | plugin: PluginService<Self, Identifier, Api>, |
| 84 | f: (plugin: Plugin<Api>) => Effect.Effect<A, E, R>, |
| 85 | ): ConfigHook<A, E, R | Self> => |
| 86 | PluginContext.use((context) => context.get(plugin).pipe(Effect.flatMap(f))); |
| 87 | |
| 88 | export const useSync = <Self, Identifier extends PluginIdentifier, Api, A>( |
| 89 | plugin: PluginService<Self, Identifier, Api>, |
| 90 | f: (plugin: Plugin<Api>) => A, |
| 91 | ): ConfigHook<A, never, Self> => |
| 92 | use(plugin, (plugin) => Effect.succeed(f(plugin))); |