Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 21 additions & 32 deletions effect-workerd/Kv.ts
Original file line number Diff line number Diff line change
@@ -1,50 +1,39 @@
import { SchemaAST, Effect, Schema as S, Option, Context } from "effect"
import { Effect, Schema as S, Option, Context, Layer } from "effect"

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

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

readonly value: S.Top

readonly binding: string
}

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

readonly definition: D
readonly layer: Layer.Layer<Self, S.SchemaError, never>

readonly "~": {
readonly key: D["key"]

readonly transcoders: {
readonly encodeKey: ReturnType<typeof S.encodeEffect<D["key"]>>
readonly decodeKey: ReturnType<typeof S.decodeEffect<D["key"]>>
readonly encodeValue: (
input: unknown,
options?: SchemaAST.ParseOptions | undefined,
) => Effect.Effect<string, S.SchemaError, D["value"]["EncodingServices"]>
readonly decodeValue: (
input: unknown,
options?: SchemaAST.ParseOptions,
) => Effect.Effect<D["value"]["Type"], S.SchemaError, D["value"]["DecodingServices"]>
readonly value: S.fromJsonString<
S.Codec<D["value"]["Type"], S.Json, D["value"]["DecodingServices"], D["value"]["EncodingServices"]>
>
}
}

export const Kv =
export const Service =
<Self>() =>
<Id extends string, D extends KvDefinition>(id: Id, definition: D): Kv<Self, Id, D> => {
const tag = Context.Service<Self, KVNamespace>()(id)

const { key, value } = definition

const transcoders = {
encodeKey: S.encodeEffect(key),
decodeKey: S.decodeEffect(key),
encodeValue: S.encodeEffect(S.fromJsonString(S.toCodecJson(value))),
decodeValue: S.decodeUnknownEffect(S.fromJsonString(S.toCodecJson(value))),
}

return Object.assign(tag, {
definition,
transcoders,
layer: Binding.layer(tag, ["get", "put", "delete", "list", "getWithMetadata"]),
layer: Binding.layer(tag, ["get", "put", "delete", "list", "getWithMetadata"])(definition.binding),
"~": {
key: definition.key,
value: S.fromJsonString(S.toCodecJson(definition.value)),
},
})
}

Expand All @@ -54,8 +43,8 @@ export const put = Effect.fnUntraced(function* <Self, Id extends string, D exten
value: D["value"]["Type"],
) {
const resolved = yield* kv
const keyEncoded = yield* kv.transcoders.encodeKey(key)
const valueEncoded = yield* kv.transcoders.encodeValue(value)
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
const valueEncoded = yield* S.encodeEffect(kv["~"].value)(value)
yield* Effect.promise(() => resolved.put(keyEncoded, valueEncoded))
})

Expand All @@ -64,19 +53,19 @@ export const get = Effect.fnUntraced(function* <Self, Id extends string, D exten
key: D["key"]["Type"],
) {
const resolved = yield* kv
const keyEncoded = yield* kv.transcoders.encodeKey(key)
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
const value = yield* Effect.promise(() => resolved.get(keyEncoded))
if (value === null) {
return Option.none()
}
return yield* kv.transcoders.decodeValue(value).pipe(Effect.map(Option.some))
return yield* S.decodeUnknownEffect(kv["~"].value)(value).pipe(Effect.map(Option.some))
})

export const remove = Effect.fnUntraced(function* <Self, Id extends string, D extends KvDefinition>(
kv: Kv<Self, Id, D>,
key: D["key"]["Type"],
) {
const resolved = yield* kv
const keyEncoded = yield* kv.transcoders.encodeKey(key)
const keyEncoded = yield* S.encodeEffect(kv["~"].key)(key)
yield* Effect.promise(() => resolved.delete(keyEncoded))
})
93 changes: 93 additions & 0 deletions effect-workerd/Vectorize.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Context, Data, Effect, Layer, Schema as S } from "effect"
import type { TopFromString } from "liminal-util/schema"

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

export interface VectorizeDefinition {
readonly binding: string

readonly id: TopFromString

readonly metadata: Record<string, S.Top & { Encoded: string | number | boolean | Array<string> }>
}

export interface Vectorize<Self, Id extends string, D extends VectorizeDefinition> extends Context.Service<
Self,
VectorizeIndex
> {
new (_: never): Context.ServiceClass.Shape<Id, VectorizeIndex>

readonly layer: Layer.Layer<Self, S.SchemaError>

readonly "~": {
readonly id: D["id"]

readonly metadata: S.Struct<D["metadata"]>
}
}

export const Service =
<Self>() =>
<Id extends string, D extends VectorizeDefinition>(id: Id, definition: D): Vectorize<Self, Id, D> => {
const tag = Context.Service<Self, VectorizeIndex>()(id)
return Object.assign(tag, {
layer: Binding.layer(tag, ["upsert", "query"])(definition.binding),
"~": {
id: definition.id,
metadata: S.Struct(definition.metadata),
},
})
}

export class VectorizeUpsertError extends Data.TaggedError("VectorizeUpsertError")<{
readonly cause: unknown
}> {}

export const upsert = Effect.fnUntraced(function* <Self, Id extends string, D extends VectorizeDefinition>(
index: Vectorize<Self, Id, D>,
id: D["id"]["Type"],
values: VectorFloatArray | number[],
metadata: S.Struct<D["metadata"]>["Type"],
) {
const i = yield* index
const idEncoded = yield* S.encodeEffect(index["~"].id)(id)
const metadataEncoded = yield* S.encodeEffect(index["~"].metadata)(metadata)
yield* Effect.tryPromise(() =>
i.upsert([
{
id: idEncoded,
values,
metadata: metadataEncoded,
},
]),
).pipe(Effect.catchTag("UnknownError", (cause) => new VectorizeUpsertError({ cause }).asEffect()))
})

export class VectorizeQueryError extends Data.TaggedError("VectorizeQueryError")<{
readonly cause: unknown
}> {}

export const query = Effect.fnUntraced(function* <Self, Id extends string, D extends VectorizeDefinition>(
index: Vectorize<Self, Id, D>,
values: VectorFloatArray | number[],
options?: VectorizeQueryOptions | undefined,
) {
const i = yield* index
const { matches } = yield* Effect.tryPromise(() => i.query(values, options)).pipe(
Effect.catchTag("UnknownError", (cause) => new VectorizeQueryError({ cause }).asEffect()),
)
const decodeId = S.decodeEffect(index["~"].id)
const decodeMetadata = S.decodeUnknownEffect(index["~"].metadata)
return yield* Effect.forEach(
matches,
({ id, metadata }) =>
Effect.all(
{
id: decodeId(id),
metadata: decodeMetadata(metadata),
},
{ concurrency: "unbounded" },
),
{ concurrency: "unbounded" },
)
})
1 change: 1 addition & 0 deletions effect-workerd/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,6 @@ export * as Images from "./Images.ts"
export * as Kv from "./Kv.ts"
export * as NativeRequest from "./NativeRequest.ts"
export * as R2 from "./R2.ts"
export * as Vectorize from "./Vectorize.ts"
export * as Worker from "./Worker.ts"
export * as WorkerLoader from "./WorkerLoader.ts"
2 changes: 1 addition & 1 deletion examples/tictactoe/api/Games.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Effect, Types } from "effect"
import { KeyValueStore } from "effect/unstable/persistence"

import type { Player } from "./TicTacToeClient.ts"
import type { Player } from "./domain.ts"

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

Expand Down
3 changes: 2 additions & 1 deletion examples/tictactoe/api/TicTacToeActor.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Schema as S } from "effect"
import { Actor } from "liminal"

import { TicTacToeClient, Player } from "./TicTacToeClient.ts"
import { Player } from "./domain.ts"
import { TicTacToeClient } from "./TicTacToeClient.ts"

export class TicTacToeActor extends Actor.Service<TicTacToeActor>()("examples/TicTacToeActor", {
client: TicTacToeClient,
Expand Down
18 changes: 3 additions & 15 deletions examples/tictactoe/api/TicTacToeClient.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
import { Schema as S } from "effect"
import { Client } from "liminal"

export const Player = S.Literals(["X", "O"])
export const Coordinate = S.Literals([0, 1, 2])
export const Coordinates = S.Tuple([Coordinate, Coordinate])

export class OutOfTurnError extends S.TaggedErrorClass<OutOfTurnError>()("OutOfTurnError", {}) {}
export class SlotTakenError extends S.TaggedErrorClass<SlotTakenError>()("SlotTakenError", {}) {}
import { Coordinates, Player } from "./domain.ts"
import * as external from "./external.ts"

export class TicTacToeClient extends Client.Service<TicTacToeClient>()("examples/TicTacToeClient", {
events: {
Expand All @@ -19,15 +15,7 @@ export class TicTacToeClient extends Client.Service<TicTacToeClient>()("examples
winner: S.optional(Player),
},
},
external: {
Move: {
payload: S.Struct({
position: Coordinates,
}),
failure: S.Never,
success: S.Void,
},
},
external,
state: {
awaitingPartner: S.Boolean,
name: Player,
Expand Down
4 changes: 2 additions & 2 deletions examples/tictactoe/api/TicTacToeNamespace.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { WorkerdActorNamespace } from "liminal/workerd"
import { ActorNamespace } from "liminal"

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

export class TicTacToeNamespace extends WorkerdActorNamespace.Service<TicTacToeNamespace>()("TicTacToeNamespace", {
export class TicTacToeNamespace extends ActorNamespace.Service<TicTacToeNamespace>()("TicTacToeNamespace", {
binding: "TICTACTOE",
actor: TicTacToeActor,
internal: {},
Expand Down
4 changes: 2 additions & 2 deletions examples/tictactoe/api/TicTacToeRuntime.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Effect, Layer } from "effect"
import { WorkerdActorRuntime } from "liminal/workerd"
import { ActorRuntime } from "liminal"

import Move from "./handleMove.ts"
import hydrate from "./hydrate.ts"
import { KvLive } from "./KvLive.ts"
import { TicTacToeNamespace } from "./TicTacToeNamespace.ts"

export class TicTacToeRuntime extends WorkerdActorRuntime.make({
export class TicTacToeRuntime extends ActorRuntime.make({
namespace: TicTacToeNamespace,
prelude: KvLive,
hydrate,
Expand Down
5 changes: 5 additions & 0 deletions examples/tictactoe/api/domain.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Schema as S } from "effect"

export const Player = S.Literals(["X", "O"])
export const Coordinate = S.Literals([0, 1, 2])
export const Coordinates = S.Tuple([Coordinate, Coordinate])
5 changes: 5 additions & 0 deletions examples/tictactoe/api/errors.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { Schema as S } from "effect"

export class OutOfTurnError extends S.TaggedErrorClass<OutOfTurnError>()("OutOfTurnError", {}) {}

export class SlotTakenError extends S.TaggedErrorClass<SlotTakenError>()("SlotTakenError", {}) {}
12 changes: 12 additions & 0 deletions examples/tictactoe/api/external.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { Schema as S } from "effect"

import { Coordinates } from "./domain.ts"
import { OutOfTurnError, SlotTakenError } from "./errors.ts"

export const Move = {
payload: S.Struct({
position: Coordinates,
}),
failure: S.Union([OutOfTurnError, SlotTakenError]),
success: S.Void,
}
8 changes: 5 additions & 3 deletions examples/tictactoe/api/handleMove.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import { Effect } from "effect"
import { handler } from "liminal"

import { OutOfTurnError, SlotTakenError } from "./errors.ts"
import { Move } from "./external.ts"
import { setBoard, getBoard } from "./Games.ts"
import { mapInternalError } from "./mapInternalError.ts"
import { TicTacToeActor } from "./TicTacToeActor.ts"
import { OutOfTurnError, SlotTakenError } from "./TicTacToeClient.ts"

// oxfmt-ignore
const LINES = [
Expand All @@ -20,8 +22,8 @@ const LINES = [
[[0, 2], [1, 1], [2, 0]],
] as const;

export default TicTacToeActor.handler(
"Move",
export default handler(
Move,
Effect.fn(function* ({ position }) {
const { currentClient, name: gameId } = yield* TicTacToeActor
const { board, turn } = yield* getBoard(gameId)
Expand Down
9 changes: 6 additions & 3 deletions examples/tictactoe/api/hydrate.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
import { Effect } from "effect"

import { TicTacToeActor } from "./TicTacToeActor.ts"
import type { TicTacToeClient } from "./TicTacToeClient.ts"

export default Effect.gen(function* () {
const { clients } = yield* TicTacToeActor
if (clients.size === 1) {
return {
awaitingPartner: true,
name: "X" as const,
}
} satisfies TicTacToeClient["State"]
} else {
yield* TicTacToeActor.others.send("GameStarted", {})
yield* Effect.addFinalizer(() =>
TicTacToeActor.others.send("GameStarted", {}).pipe(Effect.catchTag("SchemaError", Effect.die)),
)
return {
awaitingPartner: false,
name: "O" as const,
}
} satisfies TicTacToeClient["State"]
}
}).pipe(Effect.orDie)
2 changes: 1 addition & 1 deletion konfik
Submodule konfik updated 2 files
+2 −2 .helix/languages.toml
+4 −0 words.txt
File renamed without changes.
Loading