1import * as GeoMaps from "@/AWS/GeoMaps";
2import * as GeoPlaces from "@/AWS/GeoPlaces";
3import * as GeoRoutes from "@/AWS/GeoRoutes";
4import * as Lambda from "@/AWS/Lambda";
5import * as Duration from "effect/Duration";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { HttpServerRequest } from "effect/http/HttpServerRequest";
9import * as HttpServerResponse from "effect/http/HttpServerResponse";
10import path from "pathe";
11
12const main = path.resolve(import.meta.dirname, "handler.ts");
13
14export class GeoTestFunction extends Lambda.Function<Lambda.Function>()(
15 "GeoTestFunction",
16) {}
17
18export default GeoTestFunction.make(
19 {
20 main,
21 functionUrl: true,
22 // Geo calls fan out to upstream providers and can exceed Lambda's 3s default.
23 timeout: Duration.seconds(30),
24 },
25 Effect.gen(function* () {
26 const searchText = yield* GeoPlaces.SearchText();
27 const geocode = yield* GeoPlaces.Geocode();
28 const reverseGeocode = yield* GeoPlaces.ReverseGeocode();
29 const autocomplete = yield* GeoPlaces.Autocomplete();
30 const calculateRoutes = yield* GeoRoutes.CalculateRoutes();
31 const calculateIsolines = yield* GeoRoutes.CalculateIsolines();
32 const getTile = yield* GeoMaps.GetTile();
33 const getStaticMap = yield* GeoMaps.GetStaticMap();
34
35 return {
36 fetch: Effect.gen(function* () {
37 const request = yield* HttpServerRequest;
38 const pathname = new URL(request.originalUrl).pathname;
39
40 // Cheap readiness route — no AWS call.
41 if (request.method === "GET" && pathname === "/ping") {
42 return yield* HttpServerResponse.json({ ok: true });
43 }
44
45 if (request.method === "GET" && pathname === "/search-text") {
46 const result = yield* searchText({
47 QueryText: "Space Needle, Seattle, WA",
48 MaxResults: 5,
49 // SearchText requires exactly one of BiasPosition,
50 // Filter.BoundingBox, or Filter.Circle. [longitude, latitude]
51 BiasPosition: [-122.3493, 47.6205],
52 });
53 const items = result.ResultItems ?? [];
54 return yield* HttpServerResponse.json({
55 count: items.length,
56 firstPosition: items[0]?.Position,
57 });
58 }
59
60 if (request.method === "GET" && pathname === "/geocode") {
61 const result = yield* geocode({
62 QueryText: "1600 Pennsylvania Ave NW, Washington, DC",
63 MaxResults: 1,
64 });
65 const items = result.ResultItems ?? [];
66 return yield* HttpServerResponse.json({
67 count: items.length,
68 position: items[0]?.Position,
69 });
70 }
71
72 if (request.method === "GET" && pathname === "/reverse-geocode") {
73 const result = yield* reverseGeocode({
74 // [longitude, latitude] — near the Space Needle, Seattle.
75 QueryPosition: [-122.3493, 47.6205],
76 MaxResults: 1,
77 });
78 const items = result.ResultItems ?? [];
79 return yield* HttpServerResponse.json({
80 count: items.length,
81 label: items[0]?.Address?.Label,
82 });
83 }
84
85 if (request.method === "GET" && pathname === "/calculate-routes") {
86 const result = yield* calculateRoutes({
87 // [longitude, latitude] — two points in Seattle.
88 Origin: [-122.339, 47.61],
89 Destination: [-122.201, 47.61],
90 TravelMode: "Car",
91 });
92 const routes = result.Routes ?? [];
93 return yield* HttpServerResponse.json({
94 count: routes.length,
95 distance: routes[0]?.Summary?.Distance,
96 duration: routes[0]?.Summary?.Duration,
97 });
98 }
99
100 if (request.method === "GET" && pathname === "/autocomplete") {
101 const result = yield* autocomplete({
102 QueryText: "1600 Pennsylvania",
103 MaxResults: 5,
104 });
105 const items = result.ResultItems ?? [];
106 return yield* HttpServerResponse.json({
107 count: items.length,
108 firstTitle: items[0]?.Title,
109 });
110 }
111
112 if (request.method === "GET" && pathname === "/calculate-isolines") {
113 const result = yield* calculateIsolines({
114 // [longitude, latitude] — downtown Seattle.
115 Origin: [-122.339, 47.61],
116 // 10-minute drive-time isochrone.
117 Thresholds: { Time: [600] },
118 TravelMode: "Car",
119 });
120 const isolines = result.Isolines ?? [];
121 return yield* HttpServerResponse.json({
122 count: isolines.length,
123 geometryCount: isolines[0]?.Geometries?.length ?? 0,
124 pricingBucket: result.PricingBucket,
125 });
126 }
127
128 if (request.method === "GET" && pathname === "/get-tile") {
129 const result = yield* getTile({
130 Tileset: "vector.basemap",
131 Z: "0",
132 X: "0",
133 Y: "0",
134 });
135 return yield* HttpServerResponse.json({
136 byteLength: result.Blob?.byteLength ?? 0,
137 contentType: result.ContentType,
138 pricingBucket: result.PricingBucket,
139 });
140 }
141
142 if (request.method === "GET" && pathname === "/get-static-map") {
143 const result = yield* getStaticMap({
144 // FileName must match ^map(@2x)?$ (it is the {FileName} path label).
145 FileName: "map",
146 // "longitude,latitude" — the Space Needle, Seattle.
147 Center: "-122.3493,47.6205",
148 Zoom: 12,
149 Width: 400,
150 Height: 300,
151 });
152 return yield* HttpServerResponse.json({
153 byteLength: result.Blob?.byteLength ?? 0,
154 contentType: result.ContentType,
155 pricingBucket: result.PricingBucket,
156 });
157 }
158
159 return yield* HttpServerResponse.json(
160 { error: "Not found", method: request.method, pathname },
161 { status: 404 },
162 );
163 }).pipe(Effect.orDie),
164 };
165 }).pipe(
166 Effect.provide(
167 Layer.mergeAll(
168 GeoPlaces.SearchTextHttp,
169 GeoPlaces.GeocodeHttp,
170 GeoPlaces.ReverseGeocodeHttp,
171 GeoPlaces.AutocompleteHttp,
172 GeoRoutes.CalculateRoutesHttp,
173 GeoRoutes.CalculateIsolinesHttp,
174 GeoMaps.GetTileHttp,
175 GeoMaps.GetStaticMapHttp,
176 ),
177 ),
178 ),
179);