1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import { CurrentRuntimeContext, sanitizeKey } from "../RuntimeContext.ts";
4import type { Function } from "./Function.ts";
5import { InvokeFunction, InvokeFunctionError } from "./InvokeFunction.ts";
6
7/**
8 * URL-only HTTP client. Does not provision an invocation credential or private route.
9 *
10 * ### Invoke a Function
11 * **Example:** Provide the HTTP implementation
12 * ```typescript
13 * const application = Effect.gen(function* () {
14 * const api = yield* Neon.InvokeFunction(target);
15 * return {
16 * fetch: Effect.gen(function* () {
17 * const response = yield* api.fetch("/health").pipe(Effect.orDie);
18 * return HttpServerResponse.text(String(response.status));
19 * }),
20 * };
21 * }).pipe(Effect.provide(Neon.InvokeFunctionHttp));
22 * ```
23 *
24 * @layer
25 * @product Function
26 * @provides Neon.InvokeFunction
27 */
28export const InvokeFunctionHttp = Layer.effect(
29 InvokeFunction,
30 Effect.gen(function* () {
31 const host = yield* CurrentRuntimeContext;
32 if (!host)
33 return yield* Effect.die(
34 new Error("Neon.InvokeFunction requires a Platform host"),
35 );
36 return Effect.fn(function* (fn: Function) {
37 const key = sanitizeKey(
38 `NEON_FUNCTION_${Array.from(fn.FQN, (character) => character.codePointAt(0)!.toString(16)).join("_")}_URL`,
39 );
40 if (!globalThis.__ALCHEMY_RUNTIME__) yield* host.set(key, fn.url);
41 const url = host
42 .get<string>(key)
43 .pipe(
44 Effect.flatMap((url) =>
45 url
46 ? Effect.succeed(url)
47 : Effect.die(new Error("Missing Neon Function URL binding")),
48 ),
49 );
50 return {
51 url,
52 fetch: (path = "/", init?: RequestInit) =>
53 Effect.gen(function* () {
54 const base = yield* url;
55 const target = yield* Effect.try({
56 try: () => new URL(path, base),
57 catch: () =>
58 new InvokeFunctionError({
59 message: "Invalid Function request path",
60 }),
61 });
62 if (target.origin !== new URL(base).origin)
63 return yield* new InvokeFunctionError({
64 message: "Function invocation must remain on the bound origin",
65 });
66 return yield* Effect.tryPromise({
67 try: (signal) =>
68 fetch(target, {
69 ...init,
70 signal: init?.signal
71 ? AbortSignal.any([signal, init.signal])
72 : signal,
73 }),
74 catch: () =>
75 new InvokeFunctionError({
76 message: "Neon Function HTTP request failed",
77 }),
78 });
79 }),
80 };
81 });
82 }),
83);