effect-cf 0.25.3 → 0.27.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # effect-cf
2
2
 
3
+ ## 0.27.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#99](https://github.com/danieljvdm/effect-cf/pull/99) [`5195812`](https://github.com/danieljvdm/effect-cf/commit/519581205c3d3b63e977515f2a33db989a5c9875) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Keep the optional `@cloudflare/computer` integration out of the root bundle so Durable Object and other unrelated consumers can bundle `effect-cf` without installing or externalizing that peer. The synchronous root namespaces could not be preserved without making esbuild resolve the optional dependency before tree-shaking, so update `ComputerWorkspace` imports to `effect-cf/computer-workspace` and `ComputerArtifacts` imports to `effect-cf/computer-artifacts`; `effect-cf/computer-workspace-host` remains unchanged.
8
+
9
+ ## 0.26.0
10
+
11
+ ### Minor Changes
12
+
13
+ - [#97](https://github.com/danieljvdm/effect-cf/pull/97) [`b7bc27e`](https://github.com/danieljvdm/effect-cf/commit/b7bc27eb03130578e13a593d9efc45bb8d3d5269) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add first-class Cloudflare Artifacts binding support through the new `Artifacts` module. Namespace lifecycle operations, repository token and fork operations, and Git commit-history, commit, and tree reads are available as typed Effects with validated bindings, structured options and results, tracing spans, and Cloudflare error identifiers.
14
+
15
+ - [#98](https://github.com/danieljvdm/effect-cf/pull/98) [`5b9a05e`](https://github.com/danieljvdm/effect-cf/commit/5b9a05e976f653eb9a9d2003fdd57379d906c871) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add Effect-native Cloudflare Computer workspaces, scoped execution handles, a Durable Object host mixin, and session-isolated Artifacts clients.
16
+
17
+ - [#94](https://github.com/danieljvdm/effect-cf/pull/94) [`8a7ec63`](https://github.com/danieljvdm/effect-cf/commit/8a7ec630ecb2ddc2b55348cb6c701f5a9ce42d3b) Thanks [@danieljvdm](https://github.com/danieljvdm)! - Add the `WebTransport` module: a truthful WebTransport/HTTP-3 boundary for Workers. `inboundTransport` decodes the HTTP protocol metadata Cloudflare's edge attaches to requests (`httpProtocol`, `clientQuicRtt`, `clientTcpRtt`) with `isHttp3` on top; `capabilities` feature-detects what the runtime provides; and `inboundSessionsUnsupported` is an explicit typed boundary for the inbound WebTransport session API that workerd does not have (cloudflare/workerd#6451).
18
+
3
19
  ## 0.25.3
4
20
 
5
21
  ### Patch Changes
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # effect-cf
2
2
 
3
- Effect-native Cloudflare primitives for Workers, Durable Objects, Containers, bindings, Cache, KV, D1, Queues, Email, Analytics Engine, Workflows, and Durable Object storage.
3
+ Effect-native Cloudflare primitives for Workers, Durable Objects, Containers, bindings, Cache, KV, D1, R2, Artifacts, Queues, Email, Analytics Engine, Workflows, and Durable Object storage.
4
4
 
5
5
  ## Install
6
6
 
@@ -18,6 +18,21 @@ pnpm add effect-cf "effect@^4.0.0-beta.105"
18
18
  npm install effect-cf "effect@^4.0.0-beta.105"
19
19
  ```
20
20
 
21
+ The Computer workspace integration is an optional peer. Install it only in
22
+ applications that import `effect-cf/computer-workspace`,
23
+ `effect-cf/computer-artifacts`, or `effect-cf/computer-workspace-host`:
24
+
25
+ ```bash
26
+ bun add "@cloudflare/computer@^0.2.0"
27
+ ```
28
+
29
+ Workspace Git operations additionally require `@platformatic/vfs`,
30
+ `@cloudflare/computer/git`'s own optional peer:
31
+
32
+ ```bash
33
+ bun add "@platformatic/vfs@^0.4.0"
34
+ ```
35
+
21
36
  ## Goal
22
37
 
23
38
  Cloudflare APIs return promises and expose platform-specific bindings. `effect-cf` wraps those boundaries as `Context`, `Layer`, and `Effect` values so application code stays inside one managed Effect runtime.
@@ -35,6 +50,10 @@ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
35
50
  - `Kv` - typed KV namespace helper
36
51
  - `D1` - typed D1 database binding helper with an `@effect/sql-d1` backed SQL layer
37
52
  - `R2` - typed R2 bucket binding helper with Effect-wrapped object and multipart operations
53
+ - `Artifacts` - typed Cloudflare Artifacts namespace and repository helpers, including repository lifecycle, tokens, and Git object reads
54
+ - `effect-cf/computer-workspace` - scoped Effect wrappers for the complete Cloudflare Computer filesystem, Git, runtime, Assets, Artifacts, and Think-compatible client surfaces
55
+ - `effect-cf/computer-artifacts` - session-isolated Artifacts repositories backed by `@cloudflare/computer/artifacts`
56
+ - `effect-cf/computer-workspace-host` - optional Durable Object host mixin, worker-shell backend wiring, and `WorkspaceServiceProxy`
38
57
  - `Hyperdrive` - typed Hyperdrive binding helper for connection strings and optional Postgres SQL integration
39
58
  - `Images` - typed Cloudflare Images binding helper with transformation APIs and optional hosted image operations
40
59
  - `Email` - typed Cloudflare Email Service binding helper for `send_email` bindings, with limit validation and typed error codes
@@ -42,6 +61,7 @@ Runtime creation belongs at Cloudflare entrypoints, not inside binding helpers.
42
61
  - `Queue` - typed Queue producer/consumer tags plus client and error types
43
62
  - `Workflow` - typed Workflow entrypoints, steps, starter clients, and instance types
44
63
  - `Rpc` - Cloudflare RPC type helpers and scoped disposal utilities
64
+ - `WebTransport` - truthful WebTransport/HTTP-3 boundary: typed runtime capabilities and decoded inbound protocol metadata
45
65
  - `WorkerConfig` - Effect `Config` helpers backed by Cloudflare `env`
46
66
  - `effect-cf/vitest` - Effect-native runners and scoped test helpers for Cloudflare's Vitest Workers pool
47
67
 
@@ -399,6 +419,168 @@ export const readArtifact = (key: string) =>
399
419
 
400
420
  Use `createMultipartUpload(...)` or `resumeMultipartUpload(...)` for large objects; returned upload handles wrap `uploadPart`, `complete`, and `abort` in Effect.
401
421
 
422
+ ## Artifacts Example
423
+
424
+ Artifacts tags expose every namespace and repository-handle binding operation as an Effect. Configure the binding under Wrangler's `artifacts` field:
425
+
426
+ ```jsonc
427
+ {
428
+ "artifacts": [
429
+ {
430
+ "binding": "ARTIFACTS",
431
+ "namespace": "default",
432
+ // Optional in local development:
433
+ "remote": true,
434
+ },
435
+ ],
436
+ }
437
+ ```
438
+
439
+ ```ts
440
+ import { Effect } from "effect";
441
+ import { Artifacts } from "effect-cf";
442
+
443
+ class Repositories extends Artifacts.Tag<Repositories>()("Repositories") {}
444
+
445
+ export const RepositoriesLayer = Repositories.layer({ binding: "ARTIFACTS" });
446
+
447
+ export const createWorkspace = Effect.gen(function* () {
448
+ const artifacts = yield* Repositories;
449
+ const created = yield* artifacts.create("agent-workspace", {
450
+ description: "Versioned agent workspace",
451
+ setDefaultBranch: "main",
452
+ });
453
+ const repo = yield* artifacts.get(created.name);
454
+ const readToken = yield* repo.createToken("read", 3600);
455
+
456
+ return {
457
+ remote: created.remote,
458
+ token: readToken.plaintext,
459
+ history: yield* repo.log({ ref: "main", limit: 10 }),
460
+ };
461
+ });
462
+ ```
463
+
464
+ `create`, `import`, and `fork` return an initial plaintext token. Treat returned tokens as secrets and avoid logging or persisting them unnecessarily.
465
+
466
+ ## Computer Workspace Example
467
+
468
+ `ComputerWorkspace` exposes the complete `@cloudflare/computer`
469
+ `WorkspaceClient` as Effects. The workspace client and every runtime execution
470
+ handle are owned by an Effect `Scope`, so RPC stubs are disposed when their
471
+ scope closes. The host mixin is a separate optional-dependency entrypoint:
472
+
473
+ ```ts
474
+ import shellCurl from "@cloudflare/computer/shell/curl";
475
+ import { Effect, Schema as S } from "effect";
476
+ import { DurableObjectDefinition } from "effect-cf";
477
+ import * as ComputerWorkspace from "effect-cf/computer-workspace";
478
+ import { withComputerWorkspace, WorkspaceServiceProxy } from "effect-cf/computer-workspace-host";
479
+
480
+ export { WorkspaceServiceProxy };
481
+
482
+ const ResearchWorkspace = DurableObjectDefinition.make("ResearchWorkspace", {
483
+ inspect: DurableObjectDefinition.method({ success: S.String }),
484
+ });
485
+
486
+ const ResearchWorkspaceLive = ResearchWorkspace.make(ComputerWorkspace.ComputerWorkspace.layer, {
487
+ rpc: {
488
+ inspect: () =>
489
+ Effect.gen(function* () {
490
+ const workspace = yield* ComputerWorkspace.ComputerWorkspace;
491
+
492
+ // Keep reads bounded when returning their contents over RPC.
493
+ const readme = yield* workspace.readFile("/repo/README.md", {
494
+ byteLength: 256 * 1024,
495
+ });
496
+ const commits = yield* workspace.git.log({ maxCount: 20 });
497
+ const run = yield* workspace.execCollect("git status --short", {
498
+ cwd: "/repo",
499
+ timeoutMillis: 30_000,
500
+ });
501
+
502
+ return `${commits.length}:${run.exitCode}:${readme.length}`;
503
+ }),
504
+ },
505
+ });
506
+
507
+ const HostedResearchWorkspace = withComputerWorkspace(ResearchWorkspaceLive, (_, state, env) => ({
508
+ storage: state.storage,
509
+ sessionId: state.id.toString(),
510
+ gitIdentity: { name: "Research Agent", email: "agent@example.test" },
511
+ artifacts: { binding: env.ARTIFACTS },
512
+ shell: {
513
+ loader: env.WORKER_LOADER,
514
+ hostBinding: "RESEARCH_WORKSPACE",
515
+ hostId: state.id.toString(),
516
+ executionContext: state,
517
+ commands: [shellCurl],
518
+ egress: { mode: "none" },
519
+ },
520
+ }));
521
+
522
+ export class ResearchWorkspaceDurableObject extends HostedResearchWorkspace {}
523
+ ```
524
+
525
+ Serialize checkout mutations such as `clone`, `fetch`, and reset/checkout
526
+ sequences with an Effect `Semaphore` when several RPC methods can update the
527
+ same working tree concurrently. Reads can remain concurrent.
528
+
529
+ The corresponding Wrangler configuration needs SQLite-backed Durable Object
530
+ storage. Worker-shell execution additionally requires the `experimental` flag,
531
+ a Worker Loader binding, the loopback namespace binding named by `hostBinding`,
532
+ and the exact `WorkspaceServiceProxy` export shown above:
533
+
534
+ ```jsonc
535
+ {
536
+ "compatibility_flags": ["nodejs_compat", "experimental"],
537
+ "worker_loaders": [{ "binding": "WORKER_LOADER" }],
538
+ "artifacts": [{ "binding": "ARTIFACTS", "namespace": "default" }],
539
+ "durable_objects": {
540
+ "bindings": [
541
+ {
542
+ "name": "RESEARCH_WORKSPACE",
543
+ "class_name": "ResearchWorkspaceDurableObject",
544
+ },
545
+ ],
546
+ },
547
+ "migrations": [
548
+ {
549
+ "tag": "v1",
550
+ "new_sqlite_classes": ["ResearchWorkspaceDurableObject"],
551
+ },
552
+ ],
553
+ }
554
+ ```
555
+
556
+ Omit `shell` and the Worker Loader configuration when only the SQLite
557
+ filesystem and Git client are needed. Supplying `artifacts` also installs the
558
+ session-scoped client at `workspace.artifacts` and makes the in-shell
559
+ `artifacts` command use the same upstream repository-prefixing convention.
560
+
561
+ Workers can use that session isolation without a Durable Object or workspace:
562
+
563
+ ```ts
564
+ import { Effect } from "effect";
565
+ import { Artifacts } from "effect-cf";
566
+ import * as ComputerArtifacts from "effect-cf/computer-artifacts";
567
+
568
+ export const createSessionRepo = (binding: Artifacts.ArtifactsBinding, sessionId: string) =>
569
+ Effect.gen(function* () {
570
+ const artifacts = yield* ComputerArtifacts.makeClient(binding, sessionId);
571
+ const created = yield* artifacts.create("build-cache", {
572
+ description: "CI artifacts",
573
+ });
574
+ const token = yield* artifacts.createToken("build-cache", "read", 3600);
575
+
576
+ return { remote: created.remote, token: token.plaintext };
577
+ });
578
+ ```
579
+
580
+ Repository prefixing, list filtering, and the argv API are delegated directly
581
+ to `@cloudflare/computer/artifacts`, keeping this API and the in-shell command
582
+ in agreement.
583
+
402
584
  ## Hyperdrive Example
403
585
 
404
586
  Hyperdrive tags expose the binding `connectionString` directly on the yielded service.
@@ -698,6 +880,36 @@ if (Worker.isWebSocketUpgrade(request)) {
698
880
 
699
881
  Use `DurableObjectRpcWebSocket.layer(...)` for Effect RPC-over-WebSocket transports. It owns protocol parsing and RPC client bookkeeping; use `DurableWebSocket` for general application sockets, rooms, presence, and broadcast flows.
700
882
 
883
+ ## WebTransport and HTTP/3
884
+
885
+ Cloudflare Workers cannot accept inbound WebTransport sessions today: workerd contains no QUIC/HTTP-3 stack, there is no `WebSocketPair`-style session API, and the maintainers state the feature "is not currently on our priority list" ([cloudflare/workerd#6451](https://github.com/cloudflare/workerd/issues/6451), [discussion #6454](https://github.com/cloudflare/workerd/discussions/6454)). Inbound HTTP/3 is a zone-level setting — the edge terminates QUIC and the Worker still receives an ordinary `fetch` Request. Workers and Durable Objects can accept inbound WebSocket connections; Durable Objects additionally support the hibernation WebSocket API.
886
+
887
+ The `WebTransport` module gives that boundary a typed shape rather than pretending otherwise:
888
+
889
+ ```ts
890
+ import { Effect, Option } from "effect";
891
+ import { WebTransport } from "effect-cf";
892
+
893
+ const fetch = Effect.fn("fetch")(function* (request: Request) {
894
+ // Decoded metadata about the browser→edge hop (HTTP/3 does not reach you).
895
+ const transport = WebTransport.inboundTransport(request);
896
+ const viaHttp3 = Option.match(transport, {
897
+ onNone: () => false,
898
+ onSome: WebTransport.isHttp3,
899
+ });
900
+
901
+ // Feature-detected runtime capabilities: { inboundSessions: false, ... }.
902
+ const capabilities = yield* WebTransport.capabilities;
903
+
904
+ // Explicit typed boundary where a future inbound session API would slot in.
905
+ // yield* WebTransport.inboundSessionsUnsupported;
906
+
907
+ return Response.json({ viaHttp3, capabilities });
908
+ });
909
+ ```
910
+
911
+ For clients that prefer WebTransport where it exists and fall back to Cloudflare-supported WebSockets, see the companion [`effect-webtransport`](../effect-webtransport) package's `Fallback` module — `examples/todo-rpc-ws` wires it up end to end.
912
+
701
913
  ## License
702
914
 
703
915
  MIT
@@ -0,0 +1,241 @@
1
+ import { t as __exportAll } from "./rolldown-runtime-D7D4PA-g.mjs";
2
+ import { a as WorkerEnvironment, t as causeMessage } from "./ErrorMessage-_lg53p3W.mjs";
3
+ import { Context, Data, Effect, Layer } from "effect";
4
+ //#region src/Binding.ts
5
+ var Binding_exports = /* @__PURE__ */ __exportAll({
6
+ BindingNotFoundError: () => BindingNotFoundError,
7
+ BindingValidationError: () => BindingValidationError,
8
+ Service: () => Service,
9
+ TypeId: () => TypeId,
10
+ layer: () => layer$1
11
+ });
12
+ /** Internal type id marker used by binding helper services. */
13
+ const TypeId = "~effect-cf/Binding";
14
+ /** Error raised when a configured binding does not exist on `env`. */
15
+ var BindingNotFoundError = class extends Data.TaggedError("BindingNotFoundError") {};
16
+ /** Error raised when a binding exists but does not match the expected shape. */
17
+ var BindingValidationError = class extends Data.TaggedError("BindingValidationError") {};
18
+ const defaultExpected = "Cloudflare binding resource";
19
+ const isPropertyTarget = (value) => (typeof value === "object" || typeof value === "function") && value !== null;
20
+ const getObjectName = (value) => {
21
+ const tag = (() => {
22
+ try {
23
+ return Object.prototype.toString.call(value).slice(8, -1);
24
+ } catch {
25
+ return typeof value;
26
+ }
27
+ })();
28
+ const constructorName = (() => {
29
+ try {
30
+ return "constructor" in value && typeof value.constructor === "function" && typeof value.constructor.name === "string" ? value.constructor.name : void 0;
31
+ } catch {
32
+ return;
33
+ }
34
+ })();
35
+ if (tag !== "Object") return tag;
36
+ if (constructorName !== void 0 && constructorName !== "" && constructorName !== "Object") return constructorName;
37
+ return tag;
38
+ };
39
+ const propertyNames = (value) => {
40
+ const names = /* @__PURE__ */ new Set();
41
+ for (const target of [value, Object.getPrototypeOf(value)]) {
42
+ if (target === null || target === Object.prototype || target === Function.prototype) continue;
43
+ try {
44
+ for (const name of Object.getOwnPropertyNames(target)) names.add(name);
45
+ } catch {
46
+ continue;
47
+ }
48
+ }
49
+ return [...names].filter((name) => name !== "constructor").sort();
50
+ };
51
+ const isMethod = (value, name) => {
52
+ try {
53
+ return typeof Reflect.get(value, name) === "function";
54
+ } catch {
55
+ return false;
56
+ }
57
+ };
58
+ const describeActual = (value) => {
59
+ if (value === null) return "null";
60
+ if (typeof value !== "object" && typeof value !== "function") return typeof value;
61
+ const names = propertyNames(value);
62
+ const methods = names.filter((name) => isMethod(value, name));
63
+ const properties = names.filter((name) => !methods.includes(name));
64
+ const details = [methods.length > 0 ? `methods ${methods.join(", ")}` : void 0, properties.length > 0 ? `properties ${properties.join(", ")}` : void 0].filter((detail) => detail !== void 0);
65
+ if (details.length === 0) return getObjectName(value);
66
+ return `${getObjectName(value)} with ${details.join("; ")}`;
67
+ };
68
+ const getBinding = (env, binding, isResource, options) => Effect.gen(function* () {
69
+ if (!isPropertyTarget(env)) {
70
+ const actual = describeActual(env);
71
+ return yield* Effect.fail(new BindingValidationError({
72
+ binding,
73
+ expected: "WorkerEnvironment object",
74
+ actual,
75
+ message: `Cloudflare binding "${binding}" failed validation. Expected WorkerEnvironment object; got ${actual}`
76
+ }));
77
+ }
78
+ const resource = Reflect.get(env, binding);
79
+ if (resource === void 0) return yield* Effect.fail(new BindingNotFoundError({
80
+ binding,
81
+ message: `Cloudflare binding "${binding}" was not found in WorkerEnvironment`
82
+ }));
83
+ if (!isResource(resource)) {
84
+ const expected = options?.expected ?? defaultExpected;
85
+ const actual = describeActual(resource);
86
+ return yield* Effect.fail(new BindingValidationError({
87
+ binding,
88
+ expected,
89
+ actual,
90
+ message: `Cloudflare binding "${binding}" failed validation. Expected ${expected}; got ${actual}`
91
+ }));
92
+ }
93
+ return resource;
94
+ });
95
+ /**
96
+ * The overloads on {@link layer} and {@link Service} guarantee that
97
+ * `Service = Resource` whenever `wrap` is absent, making the fallback cast
98
+ * safe.
99
+ */
100
+ const makeBindingLayer = (tag, binding, isResource, wrap, options) => Layer.effect(tag, Effect.gen(function* () {
101
+ const env = yield* WorkerEnvironment;
102
+ const resource = yield* getBinding(env, binding, isResource, options);
103
+ return wrap === void 0 ? resource : wrap(resource);
104
+ }));
105
+ function layer$1(tag, binding, isResource, wrap, options) {
106
+ return makeBindingLayer(tag, binding, isResource, wrap, options);
107
+ }
108
+ const Service = () => {
109
+ function makeService(id, binding, isResource, wrap, options) {
110
+ const tag = Context.Service()(`effect-cf/Binding/${id}`);
111
+ const serviceLayer = makeBindingLayer(tag, binding, isResource, wrap, options);
112
+ return Object.assign(tag, {
113
+ [TypeId]: TypeId,
114
+ id,
115
+ binding,
116
+ layer: serviceLayer
117
+ });
118
+ }
119
+ return makeService;
120
+ };
121
+ //#endregion
122
+ //#region src/Artifacts.ts
123
+ var Artifacts_exports = /* @__PURE__ */ __exportAll({
124
+ ArtifactsOperationError: () => ArtifactsOperationError,
125
+ Tag: () => Tag,
126
+ artifactsErrorCodes: () => artifactsErrorCodes,
127
+ artifactsErrorNumericCodes: () => artifactsErrorNumericCodes,
128
+ isArtifactsBinding: () => isArtifactsBinding,
129
+ isArtifactsRepoBinding: () => isArtifactsRepoBinding,
130
+ layer: () => layer,
131
+ make: () => make,
132
+ makeClient: () => makeClient
133
+ });
134
+ const expectedArtifactsBinding = "Artifacts binding with create(), get(), import(), list(), and delete()";
135
+ const expectedArtifactsRepo = "Artifacts repo handle with createToken(), listTokens(), revokeToken(), fork(), log(), readCommit(), and readTree()";
136
+ /** Documented string error codes returned by Cloudflare Artifacts. */
137
+ const artifactsErrorCodes = [
138
+ "ALREADY_EXISTS",
139
+ "NOT_FOUND",
140
+ "IMPORT_IN_PROGRESS",
141
+ "FORK_IN_PROGRESS",
142
+ "INVALID_INPUT",
143
+ "INVALID_REPO_NAME",
144
+ "INVALID_TTL",
145
+ "INVALID_URL",
146
+ "REMOTE_AUTH_REQUIRED",
147
+ "UPSTREAM_UNAVAILABLE",
148
+ "MEMORY_LIMIT",
149
+ "INTERNAL_ERROR"
150
+ ];
151
+ /** Documented numeric error codes returned by Cloudflare Artifacts. */
152
+ const artifactsErrorNumericCodes = {
153
+ ALREADY_EXISTS: 10201,
154
+ NOT_FOUND: 10200,
155
+ IMPORT_IN_PROGRESS: 10302,
156
+ FORK_IN_PROGRESS: 10303,
157
+ INVALID_INPUT: 10100,
158
+ INVALID_REPO_NAME: 10101,
159
+ INVALID_TTL: 10103,
160
+ INVALID_URL: 10104,
161
+ REMOTE_AUTH_REQUIRED: 10106,
162
+ UPSTREAM_UNAVAILABLE: 10401,
163
+ MEMORY_LIMIT: 10402,
164
+ INTERNAL_ERROR: 10400
165
+ };
166
+ /** Error raised when a Cloudflare Artifacts operation fails. */
167
+ var ArtifactsOperationError = class extends Data.TaggedError("ArtifactsOperationError") {
168
+ get message() {
169
+ const code = this.code === void 0 ? "" : ` (${this.code})`;
170
+ return `Artifacts ${this.operation} failed for binding "${this.binding}"${code}: ${causeMessage(this.cause)}`;
171
+ }
172
+ };
173
+ const artifactsErrorDetails = (cause) => {
174
+ if (typeof cause !== "object" || cause === null) return {};
175
+ const code = Reflect.get(cause, "code");
176
+ const numericCode = Reflect.get(cause, "numericCode");
177
+ return {
178
+ ...typeof code === "string" ? { code } : {},
179
+ ...typeof numericCode === "number" ? { numericCode } : {}
180
+ };
181
+ };
182
+ const artifactsError = (binding, operation, cause) => new ArtifactsOperationError({
183
+ binding,
184
+ operation,
185
+ cause,
186
+ ...artifactsErrorDetails(cause)
187
+ });
188
+ const tryArtifactsPromise = (binding, operation, evaluate) => Effect.tryPromise({
189
+ try: evaluate,
190
+ catch: (cause) => artifactsError(binding, operation, cause)
191
+ });
192
+ const spanOptions = (binding, operation) => ({ attributes: {
193
+ binding,
194
+ operation
195
+ } });
196
+ const hasFunction = (value, key) => typeof Reflect.get(value, key) === "function";
197
+ /** Tests whether a value exposes the complete documented Artifacts repo handle API. */
198
+ const isArtifactsRepoBinding = (value) => typeof value === "object" && value !== null && hasFunction(value, "createToken") && hasFunction(value, "listTokens") && hasFunction(value, "revokeToken") && hasFunction(value, "fork") && hasFunction(value, "log") && hasFunction(value, "readCommit") && hasFunction(value, "readTree");
199
+ /** Tests whether a value exposes the complete documented Artifacts namespace API. */
200
+ const isArtifactsBinding = (value) => typeof value === "object" && value !== null && hasFunction(value, "create") && hasFunction(value, "get") && hasFunction(value, "import") && hasFunction(value, "list") && hasFunction(value, "delete");
201
+ const wrapRepo = (binding, repo) => ({
202
+ id: repo.id,
203
+ name: repo.name,
204
+ description: repo.description,
205
+ defaultBranch: repo.defaultBranch,
206
+ createdAt: repo.createdAt,
207
+ updatedAt: repo.updatedAt,
208
+ lastPushAt: repo.lastPushAt,
209
+ source: repo.source,
210
+ readOnly: repo.readOnly,
211
+ remote: repo.remote,
212
+ createToken: Effect.fn("Artifacts.createToken", spanOptions(binding, "createToken"))((scope, ttl) => tryArtifactsPromise(binding, "createToken", () => repo.createToken(scope, ttl))),
213
+ listTokens: tryArtifactsPromise(binding, "listTokens", () => repo.listTokens()).pipe(Effect.withSpan("Artifacts.listTokens", spanOptions(binding, "listTokens"))),
214
+ revokeToken: Effect.fn("Artifacts.revokeToken", spanOptions(binding, "revokeToken"))((tokenOrId) => tryArtifactsPromise(binding, "revokeToken", () => repo.revokeToken(tokenOrId))),
215
+ fork: Effect.fn("Artifacts.fork", spanOptions(binding, "fork"))((name, options) => tryArtifactsPromise(binding, "fork", () => repo.fork(name, options))),
216
+ log: Effect.fn("Artifacts.log", spanOptions(binding, "log"))((options) => tryArtifactsPromise(binding, "log", () => repo.log(options))),
217
+ readCommit: Effect.fn("Artifacts.readCommit", spanOptions(binding, "readCommit"))((hash) => tryArtifactsPromise(binding, "readCommit", () => repo.readCommit(hash))),
218
+ readTree: Effect.fn("Artifacts.readTree", spanOptions(binding, "readTree"))((hash) => tryArtifactsPromise(binding, "readTree", () => repo.readTree(hash))),
219
+ raw: repo
220
+ });
221
+ const makeClient = (definition) => (artifacts) => ({
222
+ definition,
223
+ create: Effect.fn("Artifacts.create", spanOptions(definition.binding, "create"))((name, options) => tryArtifactsPromise(definition.binding, "create", () => artifacts.create(name, options))),
224
+ get: Effect.fn("Artifacts.get", spanOptions(definition.binding, "get"))((name) => tryArtifactsPromise(definition.binding, "get", () => artifacts.get(name)).pipe(Effect.flatMap((repo) => isArtifactsRepoBinding(repo) ? Effect.succeed(wrapRepo(definition.binding, repo)) : Effect.fail(artifactsError(definition.binding, "get", /* @__PURE__ */ new TypeError(`Expected ${expectedArtifactsRepo}`)))))),
225
+ import: Effect.fn("Artifacts.import", spanOptions(definition.binding, "import"))((params) => tryArtifactsPromise(definition.binding, "import", () => artifacts.import(params))),
226
+ list: Effect.fn("Artifacts.list", spanOptions(definition.binding, "list"))((options) => tryArtifactsPromise(definition.binding, "list", () => artifacts.list(options))),
227
+ delete: Effect.fn("Artifacts.delete", spanOptions(definition.binding, "delete"))((name) => tryArtifactsPromise(definition.binding, "delete", () => artifacts.delete(name))),
228
+ rawUnsafe: Effect.succeed(artifacts)
229
+ });
230
+ const layer = (tag, definition) => layer$1(tag, definition.binding, isArtifactsBinding, makeClient(definition), { expected: expectedArtifactsBinding });
231
+ const make = (id) => Tag()(id);
232
+ const Tag = () => (id) => {
233
+ const tag = Context.Service()(id);
234
+ const makeLayer = (definition) => layer(tag, definition);
235
+ return Object.assign(tag, {
236
+ id,
237
+ layer: makeLayer
238
+ });
239
+ };
240
+ //#endregion
241
+ export { layer$1 as a, Service as i, Artifacts_exports as n, Binding_exports as r, ArtifactsOperationError as t };