@emergente-labs/effect-cloudflare
v0.3.0
Published
Effect-TS toolkit for Cloudflare Workers: fetch/queue handlers, config provider bridge, tiered caching, circuit breaker, and crypto utilities
Maintainers
Readme
@emergente-labs/effect-cloudflare
Effect-TS toolkit for Cloudflare Workers. Composable building blocks for running Effect apps on the edge.
Installation
npm install @emergente-labs/effect-cloudflare effect @effect/platformFeatures
- Worker handlers --
toFetchHandlerandtoQueueHandlerto wire up Effect services to Workers - Config bridge --
cloudflareConfigProvidermaps Worker env bindings toEffect.ConfigProvider - KV cache -- Effect service with in-memory dedupe and operation timeouts
- R2 cache -- TTL-based caching with metadata, tag invalidation, and stale-while-revalidate
- Tiered cache -- Memory -> KV -> R2 with automatic fallback, promotion, and request coalescing
- Circuit breaker -- KV-backed state that persists across isolate restarts
- Crypto utilities -- AES-GCM encryption and SHA-256 checksums via Web Crypto API
- Content type inference -- Built-in MIME type detection from file paths
Quick Start
import * as HttpRouter from "@effect/platform/HttpRouter";
import { Effect, Layer } from "effect";
import {
workerContextFactory,
toFetchHandler,
} from "@emergente-labs/effect-cloudflare";
interface Env {
MY_KV: KVNamespace;
}
const { makeWorkerContext } = workerContextFactory<[Env, ExecutionContext]>();
const app = HttpRouter.empty;
const layer = Layer.empty;
export default {
fetch: toFetchHandler(app, { layer, makeWorkerContext }),
};KV Cache
import { KvCaching, CloudflareKvNamespace } from "@emergente-labs/effect-cloudflare";
import { Effect, Layer } from "effect";
// Provide your KV namespace
const kvLayer = Layer.succeed(CloudflareKvNamespace, env.MY_KV);
const caching = KvCaching.Default.pipe(Layer.provide(kvLayer));
const program = Effect.gen(function* () {
const kv = yield* KvCaching;
const value = yield* kv.get("my-key");
yield* kv.put(["my-key", "my-value", { expirationTtl: 3600 }]);
});R2 Cache
import { R2Caching, CloudflareR2Bucket } from "@emergente-labs/effect-cloudflare";
import { Effect, Layer } from "effect";
// Provide your R2 bucket
const r2Layer = Layer.succeed(CloudflareR2Bucket, env.MY_R2_BUCKET);
const caching = R2Caching.Default.pipe(Layer.provide(r2Layer));
const program = Effect.gen(function* () {
const r2 = yield* R2Caching;
// Store with TTL and tags
yield* r2.put("assets/logo.png", imageData, {
ttl: 86400,
contentType: "image/png",
tags: ["assets", "images"],
});
// Retrieve (returns Option)
const result = yield* r2.get("assets/logo.png");
// Invalidate by tag
yield* r2.invalidateByTag("assets");
});Tiered Cache
Memory -> KV -> R2 with automatic promotion and request coalescing:
import {
TieredCache,
TieredCacheConfig,
CloudflareKvNamespace,
CloudflareR2Bucket,
RequestContext,
inferContentType,
} from "@emergente-labs/effect-cloudflare";
import { Effect, Layer } from "effect";
// Optional config (defaults to enabled)
const configLayer = Layer.succeed(TieredCacheConfig, {
enabled: true,
defaultKvTtl: 3600,
memoryCacheCapacity: 100,
});
// RequestContext wires background writes to Cloudflare's waitUntil
const requestLayer = Layer.succeed(RequestContext, {
scheduleBackgroundWork: (p) => executionCtx.waitUntil(p),
});
const program = Effect.gen(function* () {
const cache = yield* TieredCache;
// String lookup across all tiers
const result = yield* cache.getString({ kvKey: "kv:key", r2Key: "r2/key" });
// result.value, result.tier, result.promoted
// Schema-validated JSON
const json = yield* cache.getJson(MySchema)({ kvKey: "kv:data", r2Key: "r2/data" });
// Binary assets with built-in content-type inference
const asset = yield* cache.getAsset(inferContentType)({
kvKey: "kv:style.css",
r2Key: "themes/style.css",
});
// asset.bytes, asset.contentType, asset.etag, asset.tier
});Circuit Breaker
KV-backed circuit breaker that persists across Worker isolate restarts:
import { CircuitBreaker, CloudflareKvNamespace } from "@emergente-labs/effect-cloudflare";
import { Effect } from "effect";
const program = Effect.gen(function* () {
const cb = yield* CircuitBreaker;
// Wrap any effect with circuit breaker protection
const result = yield* cb.withCircuitBreaker("my-api", fetchFromApi());
// With automatic retries and exponential backoff
const resilient = yield* cb.withCircuitBreakerAndRetry("my-api", fetchFromApi());
});Config Provider Bridge
Map Worker env bindings to Effect's ConfigProvider:
import { makeCloudflareConfigLayer } from "@emergente-labs/effect-cloudflare";
import { Config, Effect } from "effect";
// In your Worker fetch handler
const configLayer = makeCloudflareConfigLayer(env);
const program = Effect.gen(function* () {
const dbUrl = yield* Config.string("DATABASE_URL");
}).pipe(Effect.provide(configLayer));Testing
All services are designed for testability. Context tags accept mock implementations:
import { KvCaching, CloudflareKvNamespace, RequestContext } from "@emergente-labs/effect-cloudflare";
import { Effect, Layer } from "effect";
// Mock KV for tests
const mockKvLayer = Layer.succeed(CloudflareKvNamespace, myMockKv);
// Mock RequestContext (no-op background work)
const mockRequestCtx = Layer.succeed(RequestContext, {
scheduleBackgroundWork: () => {},
});License
MIT
