1import * as Effect from "effect/Effect";
2import * as Layer from "effect/Layer";
3import { Worker, WorkerEnvironment } from "../Workers/Worker.ts";
4import { type Namespace as ArtifactsLike } from "./Namespace.ts";
5import {
6 ArtifactsError,
7 ReadNamespace,
8 ReadWriteNamespace,
9 type ReadWriteNamespaceClient,
10 type RepoClient,
11 WriteNamespace,
12} from "./ReadWriteNamespace.ts";
13
14const tryPromise = <T>(
15 fn: () => Promise<T>,
16): Effect.Effect<T, ArtifactsError> =>
17 Effect.tryPromise({
18 try: fn,
19 catch: (error: any) =>
20 new ArtifactsError({
21 message: error?.message ?? "Unknown error",
22 cause: error,
23 }),
24 });
25
26const wrapRepo = (raw: ArtifactsRepo): RepoClient => ({
27 raw,
28 createToken: (scope, ttl) => tryPromise(() => raw.createToken(scope, ttl)),
29 listTokens: () => tryPromise(() => raw.listTokens()),
30 revokeToken: (tokenOrId) => tryPromise(() => raw.revokeToken(tokenOrId)),
31 fork: (name, opts) => tryPromise(() => raw.fork(name, opts)),
32});
33
34/**
35 * Builds the full Artifacts client over the native worker binding. Each access
36 * level (Read / Write / ReadWrite) returns this same object typed to its subset
37 * — least-privilege by construction at the call site.
38 */
39const makeArtifactsClient = (
40 env: Record<string, Artifacts>,
41 namespace: ArtifactsLike,
42): ReadWriteNamespaceClient => {
43 const raw = Effect.sync(() => env[namespace.name]!);
44 const use = <T>(
45 fn: (raw: Artifacts) => Promise<T>,
46 ): Effect.Effect<T, ArtifactsError> =>
47 raw.pipe(Effect.flatMap((raw) => tryPromise(() => fn(raw))));
48 return {
49 raw,
50 create: (name, opts) => use((raw) => raw.create(name, opts)),
51 get: (name) =>
52 use((raw) => raw.get(name)).pipe(
53 Effect.flatMap((repo) =>
54 repo == null
55 ? Effect.fail(
56 new ArtifactsError({
57 message: `Artifacts repo '${name}' not found`,
58 cause: new Error("not_found"),
59 }),
60 )
61 : Effect.succeed(wrapRepo(repo as ArtifactsRepo)),
62 ),
63 ),
64 list: (opts) => use((raw) => raw.list(opts)),
65 delete: (name) => use((raw) => raw.delete(name)),
66 import: (opts) => use((raw) => raw.import(opts)),
67 };
68};
69
70const makeBinding = <Self>(tag: Self) =>
71 Layer.effect(
72 tag as any,
73 Effect.gen(function* () {
74 const env = yield* WorkerEnvironment;
75 const host = yield* Worker;
76 return Effect.fn(function* (namespace: ArtifactsLike) {
77 if (!globalThis.__ALCHEMY_RUNTIME__) {
78 yield* host.bind(namespace.name, {
79 bindings: [
80 {
81 type: "artifacts",
82 name: namespace.name,
83 namespace: namespace.namespace,
84 } as any,
85 ],
86 });
87 }
88 return makeArtifactsClient(env as Record<string, Artifacts>, namespace);
89 });
90 }),
91 ) as Layer.Layer<Self, never, Worker | WorkerEnvironment>;
92
93/** Read-only Artifacts binding (`get`/`list`/`raw`). */
94export const ReadNamespaceBinding = makeBinding(ReadNamespace);
95/** Write Artifacts binding (`create`/`delete`/`import`). */
96export const WriteNamespaceBinding = makeBinding(WriteNamespace);
97/** Full read + write Artifacts binding. */
98export const ReadWriteNamespaceBinding = makeBinding(ReadWriteNamespace);