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 +16 -0
- package/README.md +213 -1
- package/dist/Artifacts-BhWrdyqX.mjs +241 -0
- package/dist/Artifacts-DcoXcRP5.d.mts +231 -0
- package/dist/ComputerArtifacts.d.mts +55 -0
- package/dist/ComputerArtifacts.mjs +95 -0
- package/dist/ComputerWorkspace.d.mts +279 -0
- package/dist/ComputerWorkspace.mjs +545 -0
- package/dist/ComputerWorkspaceHost.d.mts +71 -0
- package/dist/ComputerWorkspaceHost.mjs +74 -0
- package/dist/ComputerWorkspaceHostRegistry-CY5Fv4Ng.mjs +8 -0
- package/dist/{DurableObjectState-CEh0DVw7.mjs → DurableObjectState-Dux8KfjG.mjs} +3 -48
- package/dist/{WorkflowBinding-D5iWzCUa.d.mts → DurableObjectState-biHrHwrh.d.mts} +7 -81
- package/dist/ErrorMessage-_lg53p3W.mjs +49 -0
- package/dist/Vitest.d.mts +2 -1
- package/dist/Vitest.mjs +2 -1
- package/dist/WorkflowBinding-zlRPCXVf.d.mts +79 -0
- package/dist/index.d.mts +80 -2
- package/dist/index.mjs +72 -123
- package/package.json +23 -1
- package/src/Artifacts.ts +519 -0
- package/src/ComputerArtifacts.ts +255 -0
- package/src/ComputerWorkspace.ts +1098 -0
- package/src/ComputerWorkspaceHost.ts +216 -0
- package/src/WebTransport.ts +123 -0
- package/src/index.ts +2 -0
- package/src/internal/ComputerWorkspaceHostRegistry.ts +10 -0
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 };
|