1import * as Context from "effect/Context";
2import * as Effect from "effect/Effect";
3import type * as Scope from "effect/Scope";
4import { SERVICE_USER_WORKER } from "./internal/constants.ts";
5import type * as Plugin from "./Plugin.ts";
6import type { RuntimeError } from "./RuntimeError.shared.ts";
7import { ConfigError } from "./RuntimeError.shared.ts";
8import type { RuntimeWorker } from "./RuntimeWorker.ts";
9import type * as WorkerdConfig from "./workerd/Config.ts";
10import type * as Workerd from "./workerd/Workerd.ts";
11
12export class PluginContext extends Context.Service<
13 PluginContext,
14 {
15 readonly worker: RuntimeWorker;
16 readonly plugins: ReadonlyMap<
17 string,
18 | Plugin.Plugin<any>
19 | Effect.Effect<Plugin.Plugin<any>, never, PluginContext>
20 >;
21 readonly get: <Self, Identifier extends Plugin.PluginIdentifier, Api>(
22 service: Plugin.PluginService<Self, Identifier, Api>,
23 ) => Effect.Effect<Plugin.Plugin<Api>, ConfigError>;
24 readonly start: (
25 ports: Workerd.WorkerdPorts,
26 ) => Effect.Effect<void, RuntimeError, Scope.Scope>;
27 readonly config: Effect.Effect<
28 {
29 entry: string | undefined;
30 sockets: Array<WorkerdConfig.Socket>;
31 services: Array<WorkerdConfig.Service>;
32 extensions: Array<WorkerdConfig.Extension>;
33 userWorker: Partial<WorkerdConfig.Worker>;
34 },
35 RuntimeError
36 >;
37 }
38>()("cloudflare-runtime/PluginContext") {}
39
40export type ConfigHook<A, E, R> = Effect.Effect<
41 A,
42 E | ConfigError,
43 R | PluginContext
44>;
45export type BindingHook<R = never> = ConfigHook<
46 WorkerdConfig.Worker_Binding,
47 never,
48 R
49>;
50
51export const make = (
52 worker: RuntimeWorker,
53 inheritedPlugins?: PluginMap,
54): Effect.Effect<PluginContext["Service"], RuntimeError> =>
55 Effect.gen(function* () {
56 const plugins = new Map<string, Plugin.Plugin<any>>();
57 const context = PluginContext.of({
58 worker,
59 plugins,
60 config: Effect.gen(function* () {
61 const configs = yield* Effect.forEach(
62 plugins.values(),
63 (plugin) =>
64 Effect.isEffect(plugin.defer)
65 ? plugin.defer.pipe(
66 Effect.map((config) => ({ ...plugin, ...config })),
67 Effect.provideService(PluginContext, context),
68 )
69 : Effect.succeed(plugin),
70 { concurrency: "unbounded" },
71 );
72 const services = configs.flatMap((config) => config.services ?? []);
73 const sockets = configs.flatMap((config) => config.sockets ?? []);
74 const extensions = configs.flatMap((config) => config.extensions ?? []);
75 const userWorker = Object.assign(
76 {},
77 ...configs.map((config) => config.userWorker ?? {}),
78 );
79 const middlewares = configs
80 .flatMap((config) => config.middlewares ?? [])
81 .sort((a, b) => (a.order ?? 0) - (b.order ?? 0));
82 return {
83 entry: middlewares[0]?.name,
84 sockets,
85 extensions,
86 userWorker,
87 services: [
88 ...services,
89 ...middlewares.map((middleware, index) => ({
90 name: middleware.name,
91 worker: {
92 ...middleware.worker,
93 bindings: [
94 ...(middleware.worker.bindings ?? []),
95 {
96 name: middleware.upstreamBindingName,
97 service: {
98 name:
99 index < middlewares.length - 1
100 ? middlewares[index + 1].name
101 : SERVICE_USER_WORKER,
102 },
103 },
104 ],
105 },
106 })),
107 ],
108 };
109 }),
110 start: Effect.fn(function* (ports) {
111 yield* Effect.forEach(plugins.values(), (plugin) => {
112 return plugin.start?.(ports) ?? Effect.void;
113 });
114 }),
115 get: Effect.fn(function* (service) {
116 const plugin = plugins.get(service.key);
117 if (!plugin) {
118 return yield* new ConfigError({
119 subtag: "PluginNotFound",
120 message: `Plugin "${service.key}" not found`,
121 hint: `The plugin "${service.key}" is not registered in the current context.`,
122 detail: {
123 name: service.key,
124 },
125 });
126 }
127 return plugin;
128 }),
129 });
130 const pluginMap: PluginMap = inheritedPlugins ?? new Map();
131 const pluginsFromContext = yield* pickPluginsFromContext();
132 for (const [key, builder] of pluginsFromContext.entries()) {
133 pluginMap.set(key, builder);
134 }
135 yield* Effect.forEach(pluginMap.entries(), ([key, builder]) =>
136 Effect.gen(function* () {
137 const plugin = Effect.isEffect(builder)
138 ? yield* builder.pipe(Effect.provideService(PluginContext, context))
139 : builder;
140 plugins.set(key, plugin);
141 }),
142 );
143 return context;
144 });
145
146export type PluginMap = Map<
147 Plugin.PluginIdentifier<string>,
148 Plugin.PluginBuilder<any>
149>;
150
151export const pickPluginsFromContext = <R = never>() =>
152 Effect.context<R>().pipe(
153 Effect.map(
154 (context): PluginMap =>
155 new Map(context.mapUnsafe.entries().filter(isPlugin)),
156 ),
157 );
158
159const isPlugin = <Identifier extends string>(
160 entry: [string, any],
161): entry is [Plugin.PluginIdentifier<Identifier>, Plugin.PluginBuilder<any>] =>
162 entry[0].startsWith("cloudflare-runtime/plugin/");
163
164export const use = Effect.fn("PluginContext.use")(
165 PluginContext.use.bind(PluginContext),
166);
167export const useSync = Effect.fn("PluginContext.useSync")(
168 PluginContext.useSync.bind(PluginContext),
169);