Skip to content

Commit 9583d0e

Browse files
authored
improve binding errors and module structure (#413)
1 parent abfd840 commit 9583d0e

21 files changed

Lines changed: 190 additions & 193 deletions

File tree

.changeset/modern-plants-rush.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"liminal-cloudflare": patch
3+
"liminal": patch
4+
"liminal-browser": patch
5+
---
6+
7+
Rework the module structure for bindings.

docs/pages/cloudflare/assets.mdx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ If you need lower-level behavior than `forward`, you can use the raw asset bindi
6363
Effect.gen(function* () {
6464
// ...
6565

66-
const assets = yield* Assets
66+
const assets = yield* Assets.Assets
6767
const response = yield* Effect.promise(() => assets.fetch(request))
6868
})
6969
```

docs/pages/cloudflare/hyperdrive.mdx

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,23 +17,16 @@ const HyperdriveLive = Hyperdrive.layer("HYPERDRIVE")
1717

1818
## What the wrapper gives you
1919

20-
- `.layer(binding)` — build a layer from the Cloudflare env binding name
21-
- `.connectionString` as `Effect<Redacted<string>, never, Hyperdrive>`
20+
- `Hyperdrive.layer(binding)` — build a layer from the Cloudflare env binding name
21+
- `Hyperdrive.connectionString` — an `Effect<Redacted<string>, never, Hyperdrive.Hyperdrive>` that yields the binding's
22+
connection string as a `Redacted<string>`
2223

2324
```ts
2425
import { Hyperdrive } from "liminal-cloudflare/bindings"
2526

26-
const connectionString = Hyperdrive.connectionString
27+
Hyperdrive.connectionString
2728
```
2829

29-
Internally, that is just:
30-
31-
```ts
32-
Hyperdrive.asEffect().pipe(Effect.map(({ connectionString }) => Redacted.make(connectionString)))
33-
```
34-
35-
So the wrapper is a thin typed shell over the raw Hyperdrive binding.
36-
3730
## Why the connection string is redacted
3831

3932
The raw Hyperdrive connection string is sensitive configuration.

docs/pages/cloudflare/kv.mdx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
# Cloudflare KV
22

3-
`Kv(...)` is the `liminal-cloudflare` wrapper for Cloudflare KV.
3+
`Kv.Kv(...)` is the `liminal-cloudflare` wrapper for Cloudflare KV.
44

55
It turns a KV namespace into a typed Effect service with schema-driven key encoding and value encoding/decoding.
66

77
## Define a KV service
88

9-
`Kv(...)` needs two things:
9+
`Kv.Kv(...)` needs two things:
1010

1111
- a schema whose encoded form is a string key
1212
- a schema for the stored value
@@ -22,7 +22,7 @@ const SessionValue = S.Struct({
2222
expiresAt: S.Date,
2323
})
2424

25-
export class SessionKv extends Kv<SessionKv>()("SessionKv", {
25+
export class SessionKv extends Kv.Kv<SessionKv>()("SessionKv", {
2626
key: SessionId,
2727
value: SessionValue,
2828
}) {}

examples/tictactoe/api/main.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ export default Worker.make({
3131
Layer.provide(HttpServer.layerServices),
3232
HttpRouter.toHttpEffect,
3333
Effect.flatMap((v) => v),
34-
Effect.provide(Layer.mergeAll(KvLive, TicTacToeRegistry.layer("TICTACTOE_REGISTRY"), Assets.layer("ASSETS"))),
34+
Effect.provide(Layer.mergeAll(KvLive, TicTacToeRegistry.layer("TICTACTOE"), Assets.layer("ASSETS"))),
3535
Effect.catchCause(() => Effect.succeed(HttpServerResponse.empty({ status: 500 }))),
3636
),
3737
prelude: Layer.empty,

examples/tictactoe/app/wrangler.jsonc

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"durable_objects": {
3030
"bindings": [
3131
{
32-
"name": "TIC_TAC_TOE",
32+
"name": "TICTACTOE",
3333
"class_name": "TicTacToeRegistry",
3434
},
3535
],
@@ -43,11 +43,4 @@
4343
"vars": {
4444
"DEV": true,
4545
},
46-
"d1_databases": [
47-
{
48-
"binding": "TIC_TAC_TOE_D1",
49-
"database_name": "tictactoe",
50-
"database_id": "4257ef71-66f7-498c-8a41-e4d9acebf667",
51-
},
52-
],
5346
}

liminal-cloudflare/ActorRegistry.ts

Lines changed: 16 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -24,13 +24,11 @@ import { logCause } from "liminal/_util/logCause"
2424
import * as Mutex from "liminal/_util/Mutex"
2525
import { type TopFromString, encodeJsonString, decodeJsonString } from "liminal/_util/schema"
2626

27-
import { Binding, DurableObjectState, NativeRequest } from "./bindings/index.ts"
27+
import { DoState, NativeRequest, Binding } from "./bindings/index.ts"
2828
import { close } from "./close.ts"
2929

3030
const { debug, span } = Diagnostic.module("cloudflare.ActorRegistry")
3131

32-
const TypeId = "~liminal/cloudflare/ActorRegistry" as const
33-
3432
export interface ActorRegistryDefinition<
3533
ActorSelf,
3634
ActorId extends string,
@@ -91,11 +89,9 @@ export interface ActorRegistry<
9189
PreludeE,
9290
RunROut,
9391
RunE,
94-
> extends Binding<RegistrySelf, RegistryId, DurableObjectNamespace, never, never, never> {
92+
> extends Context.Service<RegistrySelf, DurableObjectNamespace> {
9593
new (state: globalThis.DurableObjectState<{}>): Context.ServiceClass.Shape<RegistryId, DurableObjectNamespace>
9694

97-
readonly [TypeId]: typeof TypeId
98-
9995
readonly definition: ActorRegistryDefinition<
10096
ActorSelf,
10197
ActorId,
@@ -113,7 +109,9 @@ export interface ActorRegistry<
113109
readonly upgrade: (
114110
name: Name["Type"],
115111
attachments: S.Struct<AttachmentFields>["Type"],
116-
) => Effect.Effect<HttpServerResponse.HttpServerResponse, S.SchemaError, RegistrySelf | NativeRequest>
112+
) => Effect.Effect<HttpServerResponse.HttpServerResponse, S.SchemaError, RegistrySelf | NativeRequest.NativeRequest>
113+
114+
readonly layer: (binding: string) => Layer.Layer<RegistrySelf, S.SchemaError, never>
117115
}
118116

119117
export const Service =
@@ -194,13 +192,13 @@ export const Service =
194192
encodeAttachments(attachments).pipe(Effect.andThen((v) => Effect.sync(() => socket.serializeAttachment(v)))),
195193
}
196194

197-
class tag extends Binding<RegistrySelf>()(id, (value): value is DurableObjectNamespace => "getByName" in value) {
195+
const tag = class tag extends Context.Service<RegistrySelf, DurableObjectNamespace>()(id) {
198196
readonly runtime
199197
readonly directory = ClientDirectory.make(actor, transport)
200198

201-
constructor(state: globalThis.DurableObjectState<{}>, env: unknown) {
202-
// @ts-ignore
203-
super(state, env)
199+
constructor(...args: [never]) {
200+
super(...args)
201+
const [state, env] = args as never as [state: globalThis.DurableObjectState<{}>, env: unknown]
204202

205203
if (hibernation) {
206204
Option.andThen(
@@ -212,7 +210,7 @@ export const Service =
212210
const baseLayer = Layer.mergeAll(
213211
prelude.pipe(Layer.provideMerge(ConfigProvider.layer(ConfigProvider.fromUnknown(env)))),
214212
FetchHttpClient.layer,
215-
Layer.succeed(DurableObjectState, state),
213+
Layer.succeed(DoState.DoState, state),
216214
Mutex.layer,
217215
)
218216

@@ -242,12 +240,12 @@ export const Service =
242240
const attachments = yield* decodeAttachmentsString(url.searchParams.get("__liminal_attachments"))
243241
if (!this.#name) {
244242
this.#name = name
245-
const state = yield* DurableObjectState
243+
const state = yield* DoState.DoState
246244
const encoded = yield* S.encodeEffect(Name)(name)
247245
yield* Effect.promise(() => state.storage.put("__liminal_name", encoded))
248246
}
249247
const { 0: webSocket, 1: server } = new WebSocketPair()
250-
const state = yield* DurableObjectState
248+
const state = yield* DoState.DoState
251249
state.acceptWebSocket(server)
252250
server.send(yield* encodeAuditionSuccess({ _tag: "Audition.Success" }))
253251
const currentClient = yield* this.directory.register(server, attachments)
@@ -330,7 +328,7 @@ export const Service =
330328
const namespace = yield* tag
331329
const nameEncoded = yield* encodeName(name)
332330
const stub = namespace.getByName(nameEncoded)
333-
const request = yield* NativeRequest
331+
const request = yield* NativeRequest.NativeRequest
334332
const protocols = yield* Effect.fromNullishOr(request.headers.get(SecWebSocketProtocol)).pipe(
335333
Effect.map(flow(String.split(","), Array.map(String.trim))),
336334
)
@@ -353,5 +351,7 @@ export const Service =
353351
return yield* Effect.promise(() => stub.fetch(new Request(url, request))).pipe(Effect.map(HttpServerResponse.raw))
354352
}, span("upgrade"))
355353

356-
return Object.assign(tag, { [TypeId]: TypeId, definition, upgrade }) as never
354+
const layer = Binding.layer(tag, ["getByName"])
355+
356+
return Object.assign(tag, { definition, upgrade, layer }) as never
357357
}

liminal-cloudflare/asset_forwarding.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { Effect, Layer } from "effect"
22
import { HttpServerResponse } from "effect/unstable/http"
33

4-
import { Assets } from "./bindings/Assets.ts"
4+
import * as Assets from "./bindings/Assets.ts"
55
import * as Worker from "./bindings/Worker.ts"
66

77
export default Worker.make({

liminal-cloudflare/bindings/Ai.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1-
import { Binding } from "./Binding.ts"
1+
import { Context } from "effect"
22

3-
export class Ai extends Binding<Ai>()("liminal/cloudflare/Ai", (v): v is globalThis.Ai => "run" in v) {}
3+
import * as Binding from "./Binding.ts"
4+
5+
export class Ai extends Context.Service<Ai, globalThis.Ai>()("liminal/cloudflare/Ai") {}
6+
7+
export const layer = Binding.layer(Ai, ["run"])
Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,21 @@
1-
import { Effect } from "effect"
1+
import { Effect, Context } from "effect"
22
import { HttpServerResponse } from "effect/unstable/http"
33

4-
import { Binding } from "./Binding.ts"
4+
import * as Binding from "./Binding.ts"
55
import { NativeRequest } from "./NativeRequest.ts"
66

7-
export class Assets extends Binding<Assets>()(
8-
"liminal/cloudflare/Assets",
9-
(value): value is { fetch: typeof fetch } => "fetch" in value,
10-
) {
11-
static readonly forward = Effect.gen({ self: this }, function* () {
12-
const assets = yield* this
13-
const request = yield* NativeRequest
14-
const response = yield* Effect.promise(() => assets.fetch(request))
15-
return HttpServerResponse.fromWeb(response)
16-
})
17-
}
7+
export class Assets extends Context.Service<
8+
Assets,
9+
{
10+
readonly fetch: typeof fetch
11+
}
12+
>()("liminal/cloudflare/Assets") {}
13+
14+
export const layer = Binding.layer(Assets, ["fetch"])
15+
16+
export const forward = Effect.gen({ self: this }, function* () {
17+
const assets = yield* Assets
18+
const request = yield* NativeRequest
19+
const response = yield* Effect.promise(() => assets.fetch(request))
20+
return HttpServerResponse.fromWeb(response)
21+
})

0 commit comments

Comments
 (0)