1import * as Cloudflare from "alchemy/Cloudflare";
2import * as Effect from "effect/Effect";
3import * as Layer from "effect/Layer";
4import * as Path from "effect/Path";
5import * as Etag from "effect/http/Etag";
6import * as HttpPlatform from "effect/http/HttpPlatform";
7import * as HttpRouter from "effect/http/HttpRouter";
8import * as HttpApiBuilder from "effect/http-api/HttpApiBuilder";
9import {
10 CloneToken,
11 CreateRepoResponse,
12 Metadata,
13 RepoApi,
14 RepoConflict,
15 RepoInfo,
16 RepoNotFound,
17} from "./Api.ts";
18import Repo from "./Repo.ts";
19import { Repos } from "./Repos.ts";
20
21// Workers don't have a FileSystem, so HttpPlatform's file-response surface
22// is stubbed. The repo API never serves files.
23const HttpPlatformStub = Layer.succeed(HttpPlatform.HttpPlatform, {
24 fileResponse: () => Effect.die("HttpPlatform.fileResponse not supported"),
25 fileWebResponse: () =>
26 Effect.die("HttpPlatform.fileWebResponse not supported"),
27});
28
29export default class Worker extends Cloudflare.Worker<Worker>()(
30 "Api",
31 {
32 main: import.meta.url,
33 observability: { enabled: true },
34 compatibility: {
35 date: "2026-08-31",
36 },
37 },
38 Effect.gen(function* () {
39 const artifacts = yield* Repos;
40 const repos = yield* Repo;
41
42 const findRepo = (name: string) =>
43 artifacts.list({ limit: 100 }).pipe(
44 Effect.flatMap((res) => {
45 const found = res.repos.find(
46 (r: { name: string }) => r.name === name,
47 );
48 return found
49 ? Effect.succeed(found)
50 : Effect.fail(new RepoNotFound({ name }));
51 }),
52 Effect.catchTag("ArtifactsError", () =>
53 Effect.fail(new RepoNotFound({ name })),
54 ),
55 );
56
57 const handlers = HttpApiBuilder.group(RepoApi, "repos", (h) =>
58 h
59 .handle("createRepo", ({ payload }) =>
60 artifacts
61 .create(payload.name, {
62 description: payload.description,
63 setDefaultBranch: "main",
64 })
65 .pipe(
66 Effect.tap(() =>
67 repos
68 .getByName(payload.name)
69 .init(payload.description ?? "")
70 .pipe(Effect.orDie),
71 ),
72 Effect.map(
73 (c) =>
74 new CreateRepoResponse({
75 name: c.name,
76 remote: c.remote,
77 token: c.token,
78 defaultBranch: c.defaultBranch,
79 }),
80 ),
81 Effect.catchTag("ArtifactsError", (err) =>
82 Effect.fail(new RepoConflict({ message: err.message })),
83 ),
84 ),
85 )
86 .handle("getRepo", ({ params }) =>
87 findRepo(params.name).pipe(
88 Effect.flatMap((found) =>
89 repos
90 .getByName(params.name)
91 .get()
92 .pipe(
93 Effect.catch(() => Effect.succeed(null)),
94 Effect.map(
95 (meta) =>
96 new RepoInfo({
97 id: found.id,
98 name: found.name,
99 description: found.description ?? null,
100 defaultBranch: found.defaultBranch,
101 remote: found.remote,
102 status: found.status,
103 readOnly: found.readOnly,
104 createdAt: found.createdAt,
105 updatedAt: found.updatedAt,
106 lastPushAt: found.lastPushAt ?? null,
107 metadata: meta ? new Metadata(meta) : null,
108 }),
109 ),
110 ),
111 ),
112 ),
113 )
114 .handle("updateRepo", ({ params, payload }) =>
115 findRepo(params.name).pipe(
116 Effect.flatMap(() =>
117 repos
118 .getByName(params.name)
119 .update({
120 description: payload.description,
121 topics: payload.topics ? [...payload.topics] : undefined,
122 })
123 .pipe(Effect.orDie),
124 ),
125 Effect.map((m) => new Metadata(m)),
126 ),
127 )
128 .handle("deleteRepo", ({ params }) =>
129 artifacts.delete(params.name).pipe(
130 Effect.asVoid,
131 Effect.catchTag("ArtifactsError", () =>
132 Effect.fail(new RepoNotFound({ name: params.name })),
133 ),
134 ),
135 )
136 .handle("starRepo", ({ params }) =>
137 findRepo(params.name).pipe(
138 Effect.flatMap(() =>
139 repos.getByName(params.name).star().pipe(Effect.orDie),
140 ),
141 Effect.map((m) => new Metadata(m)),
142 ),
143 )
144 .handle("cloneToken", ({ params, payload }) =>
145 artifacts.get(params.name).pipe(
146 Effect.flatMap((handle) =>
147 handle.createToken(payload.scope ?? "read", payload.ttl ?? 3600),
148 ),
149 Effect.map(
150 (t) =>
151 new CloneToken({
152 id: t.id,
153 plaintext: t.plaintext,
154 scope: t.scope as "read" | "write",
155 expiresAt: t.expiresAt,
156 }),
157 ),
158 Effect.catchTag("ArtifactsError", () =>
159 Effect.fail(new RepoNotFound({ name: params.name })),
160 ),
161 ),
162 ),
163 );
164
165 return {
166 fetch: HttpApiBuilder.layer(RepoApi).pipe(
167 Layer.provide(handlers),
168 Layer.provide([Etag.layer, HttpPlatformStub, Path.layer]),
169 HttpRouter.toHttpEffect,
170 ),
171 };
172 }).pipe(Effect.provide(Layer.mergeAll(Binding))),
173) {}