Skip to content

Commit 16f8b41

Browse files
authored
flatten exports, vectorize bindings, fix browser actor transcoding (#443)
1 parent d60b6c4 commit 16f8b41

29 files changed

Lines changed: 384 additions & 246 deletions

effect-workerd/Kv.ts

Lines changed: 21 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,50 +1,39 @@
1-
import { SchemaAST, Effect, Schema as S, Option, Context } from "effect"
1+
import { Effect, Schema as S, Option, Context, Layer } from "effect"
22

33
import * as Binding from "./Binding.ts"
44

55
export interface KvDefinition {
66
readonly key: S.Top & { Encoded: string }
77

88
readonly value: S.Top
9+
10+
readonly binding: string
911
}
1012

1113
export interface Kv<Self, Id extends string, D extends KvDefinition> extends Context.Service<Self, KVNamespace> {
1214
new (_: never): Context.ServiceClass.Shape<Id, KVNamespace>
1315

14-
readonly definition: D
16+
readonly layer: Layer.Layer<Self, S.SchemaError, never>
17+
18+
readonly "~": {
19+
readonly key: D["key"]
1520

16-
readonly transcoders: {
17-
readonly encodeKey: ReturnType<typeof S.encodeEffect<D["key"]>>
18-
readonly decodeKey: ReturnType<typeof S.decodeEffect<D["key"]>>
19-
readonly encodeValue: (
20-
input: unknown,
21-
options?: SchemaAST.ParseOptions | undefined,
22-
) => Effect.Effect<string, S.SchemaError, D["value"]["EncodingServices"]>
23-
readonly decodeValue: (
24-
input: unknown,
25-
options?: SchemaAST.ParseOptions,
26-
) => Effect.Effect<D["value"]["Type"], S.SchemaError, D["value"]["DecodingServices"]>
21+
readonly value: S.fromJsonString<
22+
S.Codec<D["value"]["Type"], S.Json, D["value"]["DecodingServices"], D["value"]["EncodingServices"]>
23+
>
2724
}
2825
}
2926

30-
export const Kv =
27+
export const Service =
3128
<Self>() =>
3229
<Id extends string, D extends KvDefinition>(id: Id, definition: D): Kv<Self, Id, D> => {
3330
const tag = Context.Service<Self, KVNamespace>()(id)
34-
35-
const { key, value } = definition
36-
37-
const transcoders = {
38-
encodeKey: S.encodeEffect(key),
39-
decodeKey: S.decodeEffect(key),
40-
encodeValue: S.encodeEffect(S.fromJsonString(S.toCodecJson(value))),
41-
decodeValue: S.decodeUnknownEffect(S.fromJsonString(S.toCodecJson(value))),
42-
}
43-
4431
return Object.assign(tag, {
45-
definition,
46-
transcoders,
47-
layer: Binding.layer(tag, ["get", "put", "delete", "list", "getWithMetadata"]),
32+
layer: Binding.layer(tag, ["get", "put", "delete", "list", "getWithMetadata"])(definition.binding),
33+
"~": {
34+
key: definition.key,
35+
value: S.fromJsonString(S.toCodecJson(definition.value)),
36+
},
4837
})
4938
}
5039

@@ -54,8 +43,8 @@ export const put = Effect.fnUntraced(function* <Self, Id extends string, D exten
5443
value: D["value"]["Type"],
5544
) {
5645
const resolved = yield* kv
57-
const keyEncoded = yield* kv.transcoders.encodeKey(key)
58-
const valueEncoded = yield* kv.transcoders.encodeValue(value)
46+
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
47+
const valueEncoded = yield* S.encodeEffect(kv["~"].value)(value)
5948
yield* Effect.promise(() => resolved.put(keyEncoded, valueEncoded))
6049
})
6150

@@ -64,19 +53,19 @@ export const get = Effect.fnUntraced(function* <Self, Id extends string, D exten
6453
key: D["key"]["Type"],
6554
) {
6655
const resolved = yield* kv
67-
const keyEncoded = yield* kv.transcoders.encodeKey(key)
56+
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
6857
const value = yield* Effect.promise(() => resolved.get(keyEncoded))
6958
if (value === null) {
7059
return Option.none()
7160
}
72-
return yield* kv.transcoders.decodeValue(value).pipe(Effect.map(Option.some))
61+
return yield* S.decodeUnknownEffect(kv["~"].value)(value).pipe(Effect.map(Option.some))
7362
})
7463

7564
export const remove = Effect.fnUntraced(function* <Self, Id extends string, D extends KvDefinition>(
7665
kv: Kv<Self, Id, D>,
7766
key: D["key"]["Type"],
7867
) {
7968
const resolved = yield* kv
80-
const keyEncoded = yield* kv.transcoders.encodeKey(key)
69+
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
8170
yield* Effect.promise(() => resolved.delete(keyEncoded))
8271
})

effect-workerd/Vectorize.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import { Context, Data, Effect, Layer, Schema as S } from "effect"
2+
import type { TopFromString } from "liminal-util/schema"
3+
4+
import * as Binding from "./Binding.ts"
5+
6+
export interface VectorizeDefinition {
7+
readonly binding: string
8+
9+
readonly id: TopFromString
10+
11+
readonly metadata: Record<string, S.Top & { Encoded: string | number | boolean | Array<string> }>
12+
}
13+
14+
export interface Vectorize<Self, Id extends string, D extends VectorizeDefinition> extends Context.Service<
15+
Self,
16+
VectorizeIndex
17+
> {
18+
new (_: never): Context.ServiceClass.Shape<Id, VectorizeIndex>
19+
20+
readonly layer: Layer.Layer<Self, S.SchemaError>
21+
22+
readonly "~": {
23+
readonly id: D["id"]
24+
25+
readonly metadata: S.Struct<D["metadata"]>
26+
}
27+
}
28+
29+
export const Service =
30+
<Self>() =>
31+
<Id extends string, D extends VectorizeDefinition>(id: Id, definition: D): Vectorize<Self, Id, D> => {
32+
const tag = Context.Service<Self, VectorizeIndex>()(id)
33+
return Object.assign(tag, {
34+
layer: Binding.layer(tag, ["upsert", "query"])(definition.binding),
35+
"~": {
36+
id: definition.id,
37+
metadata: S.Struct(definition.metadata),
38+
},
39+
})
40+
}
41+
42+
export class VectorizeUpsertError extends Data.TaggedError("VectorizeUpsertError")<{
43+
readonly cause: unknown
44+
}> {}
45+
46+
export const upsert = Effect.fnUntraced(function* <Self, Id extends string, D extends VectorizeDefinition>(
47+
index: Vectorize<Self, Id, D>,
48+
id: D["id"]["Type"],
49+
values: VectorFloatArray | number[],
50+
metadata: S.Struct<D["metadata"]>["Type"],
51+
) {
52+
const i = yield* index
53+
const idEncoded = yield* S.encodeEffect(index["~"].id)(id)
54+
const metadataEncoded = yield* S.encodeEffect(index["~"].metadata)(metadata)
55+
yield* Effect.tryPromise(() =>
56+
i.upsert([
57+
{
58+
id: idEncoded,
59+
values,
60+
metadata: metadataEncoded,
61+
},
62+
]),
63+
).pipe(Effect.catchTag("UnknownError", (cause) => new VectorizeUpsertError({ cause }).asEffect()))
64+
})
65+
66+
export class VectorizeQueryError extends Data.TaggedError("VectorizeQueryError")<{
67+
readonly cause: unknown
68+
}> {}
69+
70+
export const query = Effect.fnUntraced(function* <Self, Id extends string, D extends VectorizeDefinition>(
71+
index: Vectorize<Self, Id, D>,
72+
values: VectorFloatArray | number[],
73+
options?: VectorizeQueryOptions | undefined,
74+
) {
75+
const i = yield* index
76+
const { matches } = yield* Effect.tryPromise(() => i.query(values, options)).pipe(
77+
Effect.catchTag("UnknownError", (cause) => new VectorizeQueryError({ cause }).asEffect()),
78+
)
79+
const decodeId = S.decodeEffect(index["~"].id)
80+
const decodeMetadata = S.decodeUnknownEffect(index["~"].metadata)
81+
return yield* Effect.forEach(
82+
matches,
83+
({ id, metadata }) =>
84+
Effect.all(
85+
{
86+
id: decodeId(id),
87+
metadata: decodeMetadata(metadata),
88+
},
89+
{ concurrency: "unbounded" },
90+
),
91+
{ concurrency: "unbounded" },
92+
)
93+
})

effect-workerd/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,6 @@ export * as Images from "./Images.ts"
88
export * as Kv from "./Kv.ts"
99
export * as NativeRequest from "./NativeRequest.ts"
1010
export * as R2 from "./R2.ts"
11+
export * as Vectorize from "./Vectorize.ts"
1112
export * as Worker from "./Worker.ts"
1213
export * as WorkerLoader from "./WorkerLoader.ts"

examples/tictactoe/api/Games.ts

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

4-
import type { Player } from "./TicTacToeClient.ts"
4+
import type { Player } from "./domain.ts"
55

66
export type Board = Types.TupleOf<3, Types.TupleOf<3, typeof Player.Type | undefined>>
77

examples/tictactoe/api/TicTacToeActor.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { Schema as S } from "effect"
22
import { Actor } from "liminal"
33

4-
import { TicTacToeClient, Player } from "./TicTacToeClient.ts"
4+
import { Player } from "./domain.ts"
5+
import { TicTacToeClient } from "./TicTacToeClient.ts"
56

67
export class TicTacToeActor extends Actor.Service<TicTacToeActor>()("examples/TicTacToeActor", {
78
client: TicTacToeClient,

examples/tictactoe/api/TicTacToeClient.ts

Lines changed: 3 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,8 @@
11
import { Schema as S } from "effect"
22
import { Client } from "liminal"
33

4-
export const Player = S.Literals(["X", "O"])
5-
export const Coordinate = S.Literals([0, 1, 2])
6-
export const Coordinates = S.Tuple([Coordinate, Coordinate])
7-
8-
export class OutOfTurnError extends S.TaggedErrorClass<OutOfTurnError>()("OutOfTurnError", {}) {}
9-
export class SlotTakenError extends S.TaggedErrorClass<SlotTakenError>()("SlotTakenError", {}) {}
4+
import { Coordinates, Player } from "./domain.ts"
5+
import * as external from "./external.ts"
106

117
export class TicTacToeClient extends Client.Service<TicTacToeClient>()("examples/TicTacToeClient", {
128
events: {
@@ -19,15 +15,7 @@ export class TicTacToeClient extends Client.Service<TicTacToeClient>()("examples
1915
winner: S.optional(Player),
2016
},
2117
},
22-
external: {
23-
Move: {
24-
payload: S.Struct({
25-
position: Coordinates,
26-
}),
27-
failure: S.Never,
28-
success: S.Void,
29-
},
30-
},
18+
external,
3119
state: {
3220
awaitingPartner: S.Boolean,
3321
name: Player,

examples/tictactoe/api/TicTacToeNamespace.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { WorkerdActorNamespace } from "liminal/workerd"
1+
import { ActorNamespace } from "liminal"
22

33
import { TicTacToeActor } from "./TicTacToeActor.ts"
44

5-
export class TicTacToeNamespace extends WorkerdActorNamespace.Service<TicTacToeNamespace>()("TicTacToeNamespace", {
5+
export class TicTacToeNamespace extends ActorNamespace.Service<TicTacToeNamespace>()("TicTacToeNamespace", {
66
binding: "TICTACTOE",
77
actor: TicTacToeActor,
88
internal: {},

examples/tictactoe/api/TicTacToeRuntime.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import { Effect, Layer } from "effect"
2-
import { WorkerdActorRuntime } from "liminal/workerd"
2+
import { ActorRuntime } from "liminal"
33

44
import Move from "./handleMove.ts"
55
import hydrate from "./hydrate.ts"
66
import { KvLive } from "./KvLive.ts"
77
import { TicTacToeNamespace } from "./TicTacToeNamespace.ts"
88

9-
export class TicTacToeRuntime extends WorkerdActorRuntime.make({
9+
export class TicTacToeRuntime extends ActorRuntime.make({
1010
namespace: TicTacToeNamespace,
1111
prelude: KvLive,
1212
hydrate,

examples/tictactoe/api/domain.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Schema as S } from "effect"
2+
3+
export const Player = S.Literals(["X", "O"])
4+
export const Coordinate = S.Literals([0, 1, 2])
5+
export const Coordinates = S.Tuple([Coordinate, Coordinate])

examples/tictactoe/api/errors.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
import { Schema as S } from "effect"
2+
3+
export class OutOfTurnError extends S.TaggedErrorClass<OutOfTurnError>()("OutOfTurnError", {}) {}
4+
5+
export class SlotTakenError extends S.TaggedErrorClass<SlotTakenError>()("SlotTakenError", {}) {}

0 commit comments

Comments
 (0)