effect-genserver 0.1.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/package.json ADDED
@@ -0,0 +1,57 @@
1
+ {
2
+ "name": "effect-genserver",
3
+ "type": "module",
4
+ "version": "0.1.0",
5
+ "publishConfig": {
6
+ "access": "public"
7
+ },
8
+ "exports": {
9
+ ".": "./dist/index.js",
10
+ "./*": "./dist/*.js",
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "dist",
15
+ "src"
16
+ ],
17
+ "license": "MIT",
18
+ "author": "Tim Smart <hello@timsmart.co>",
19
+ "homepage": "https://github.com/tim-smart/effect-genserver",
20
+ "repository": {
21
+ "type": "git",
22
+ "url": "https://github.com/tim-smart/effect-genserver.git"
23
+ },
24
+ "dependencies": {},
25
+ "peerDependencies": {
26
+ "effect": "4.0.0-beta.48"
27
+ },
28
+ "devDependencies": {
29
+ "@changesets/changelog-github": "^0.6.0",
30
+ "@changesets/cli": "^2.29.8",
31
+ "@effect/language-service": "^0.85.1",
32
+ "@effect/vitest": "4.0.0-beta.48",
33
+ "@types/node": "^25.6.0",
34
+ "@typescript/native-preview": "7.0.0-dev.20260412.1",
35
+ "effect": "4.0.0-beta.48",
36
+ "husky": "^9.1.7",
37
+ "lint-staged": "^16.4.0",
38
+ "oxlint": "^1.57.0",
39
+ "prettier": "^3.8.2",
40
+ "typescript": "^6.0.2",
41
+ "vitest": "^4.1.1"
42
+ },
43
+ "lint-staged": {
44
+ "*.{ts,tsx}": [
45
+ "oxlint --fix",
46
+ "prettier --write"
47
+ ],
48
+ "*.{json,md,yml,yaml}": [
49
+ "prettier --write"
50
+ ]
51
+ },
52
+ "scripts": {
53
+ "check": "oxlint -c .oxlintrc.json --fix && pnpm tsc -b tsconfig.json",
54
+ "test": "vitest run",
55
+ "build": "tsc -b tsconfig.json"
56
+ }
57
+ }
@@ -0,0 +1,112 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import type * as Rpc from "effect/unstable/rpc/Rpc"
5
+ import * as Effect from "effect/Effect"
6
+ import type * as Schema from "effect/Schema"
7
+ import * as Layer from "effect/Layer"
8
+ import * as GenServer from "./GenServer.ts"
9
+ import * as Atom from "effect/unstable/reactivity/Atom"
10
+ import { identity, pipe } from "effect/Function"
11
+ import * as Stream from "effect/Stream"
12
+ import * as AsyncResult from "effect/unstable/reactivity/AsyncResult"
13
+ import * as Duration from "effect/Duration"
14
+
15
+ /**
16
+ * @since 1.0.0
17
+ * @category Atom
18
+ */
19
+ export const make = <
20
+ State extends Schema.Top,
21
+ Rpcs extends Rpc.Any,
22
+ E,
23
+ InitialState extends State["Type"] | undefined = undefined,
24
+ >(
25
+ server: GenServer.GenServer<State, Rpcs>,
26
+ layer:
27
+ | Layer.Layer<
28
+ GenServer.ToHandler<Rpcs> | GenServer.InitialState,
29
+ E,
30
+ GenServer.SendDiscard
31
+ >
32
+ | ((
33
+ get: Atom.AtomContext,
34
+ ) => Layer.Layer<
35
+ GenServer.ToHandler<Rpcs> | GenServer.InitialState,
36
+ E,
37
+ GenServer.SendDiscard
38
+ >),
39
+ options?: {
40
+ readonly memoMap?: Layer.MemoMap | undefined
41
+ readonly idleTTL?: Duration.Input | undefined
42
+ readonly initialState?: InitialState | undefined
43
+ },
44
+ ): {
45
+ readonly actor: Atom.Atom<
46
+ AsyncResult.AsyncResult<GenServer.Actor<State, Rpcs>, E>
47
+ >
48
+ readonly send: <Tag extends Rpcs["_tag"]>(
49
+ tag: Tag,
50
+ options?: {
51
+ readonly concurrent?: boolean | undefined
52
+ },
53
+ ) => Atom.AtomResultFn<
54
+ Rpc.PayloadConstructor<Rpc.ExtractTag<Rpcs, Tag>>,
55
+ Rpc.Success<Rpc.ExtractTag<Rpcs, Tag>>,
56
+ E | Schema.Schema.Type<Rpc.ErrorSchema<Rpc.ExtractTag<Rpcs, Tag>>>
57
+ >
58
+ readonly state: Atom.Atom<
59
+ [InitialState] extends [undefined]
60
+ ? AsyncResult.AsyncResult<State["Type"], E>
61
+ : State["Type"]
62
+ >
63
+ } => {
64
+ const memoMap = options?.memoMap ?? Atom.runtime.memoMap
65
+
66
+ const actor = Atom.make((get) => {
67
+ const layer_ = typeof layer === "function" ? layer(get) : layer
68
+ return GenServer.makeActor(server, layer_).pipe(
69
+ Effect.provideService(Layer.CurrentMemoMap, memoMap),
70
+ )
71
+ }).pipe(Atom.setIdleTTL(options?.idleTTL ?? Duration.zero))
72
+
73
+ const state = pipe(
74
+ Atom.make((get) =>
75
+ pipe(
76
+ get.result(actor),
77
+ Effect.map((actor) => actor.changes),
78
+ Stream.unwrap,
79
+ ),
80
+ ),
81
+ options?.initialState
82
+ ? Atom.map(AsyncResult.getOrElse(() => options.initialState))
83
+ : identity,
84
+ ) as Atom.Atom<AsyncResult.AsyncResult<State["Type"], E>>
85
+
86
+ const sendFamily = <
87
+ Tag extends Rpcs["_tag"],
88
+ Rpc = Rpc.ExtractTag<Rpcs, Tag>,
89
+ >([tag, concurrent]: [Tag, boolean]): Atom.AtomResultFn<
90
+ Rpc.PayloadConstructor<Rpc>,
91
+ Rpc.Success<Rpc>,
92
+ Rpc.Error<Rpc> | E
93
+ > => {
94
+ return Atom.fn(
95
+ (payload, get) =>
96
+ pipe(
97
+ get.result(actor),
98
+ Effect.flatMap((actor) => actor.send(tag, payload)),
99
+ ),
100
+ { concurrent },
101
+ )
102
+ }
103
+
104
+ const send = <Tag extends Rpcs["_tag"]>(
105
+ tag: Tag,
106
+ options?: {
107
+ readonly concurrent?: boolean | undefined
108
+ },
109
+ ) => sendFamily([tag, options?.concurrent ?? false])
110
+
111
+ return { actor, send, state }
112
+ }
@@ -0,0 +1,96 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import type * as Rpc from "effect/unstable/rpc/Rpc"
5
+ import * as Effect from "effect/Effect"
6
+ import type * as Schema from "effect/Schema"
7
+ import type * as Layer from "effect/Layer"
8
+ import type * as Scope from "effect/Scope"
9
+ import * as Context from "effect/Context"
10
+ import type * as RpcSchema from "effect/unstable/rpc/RpcSchema"
11
+ import * as Entity from "effect/unstable/cluster/Entity"
12
+ import type * as Envelope from "effect/unstable/cluster/Envelope"
13
+ import * as GenServer from "./GenServer.ts"
14
+
15
+ /**
16
+ * @since 1.0.0
17
+ * @category Entity
18
+ */
19
+ export const entity = <
20
+ const Type extends string,
21
+ State extends Schema.Top,
22
+ Rpcs extends Rpc.Any,
23
+ >(
24
+ type: Type,
25
+ schema: GenServer.GenServer<State, Rpcs>,
26
+ ): Entity.Entity<
27
+ Type,
28
+ | Rpcs
29
+ | Rpc.Rpc<
30
+ "GenServerChanges",
31
+ Schema.Void,
32
+ RpcSchema.Stream<State, Schema.Never>
33
+ >
34
+ > =>
35
+ Entity.fromRpcGroup(
36
+ type,
37
+ schema.protocol.add(GenServer.RpcStateChanges(schema)),
38
+ )
39
+
40
+ /**
41
+ * @since 1.0.0
42
+ * @category Entity
43
+ */
44
+ export type EntityHandlersFrom<Rpcs extends Rpc.Any> = {
45
+ readonly [Current in Rpcs as Current["_tag"]]: (
46
+ envelope: Envelope.Request<Current>,
47
+ ) => Rpc.WrapperOr<Rpc.ResultFrom<Current, never>>
48
+ }
49
+
50
+ /**
51
+ * @since 1.0.0
52
+ * @category Entity
53
+ */
54
+ export const entityHandlers = Effect.fnUntraced(function* <
55
+ State extends Schema.Top,
56
+ Rpcs extends Rpc.Any,
57
+ E,
58
+ R,
59
+ >(
60
+ schema: GenServer.GenServer<State, Rpcs>,
61
+ layer: Layer.Layer<GenServer.ToHandler<Rpcs> | GenServer.InitialState, E, R>,
62
+ ): Effect.fn.Return<
63
+ EntityHandlersFrom<
64
+ | Rpcs
65
+ | Rpc.Rpc<
66
+ "GenServerChanges",
67
+ Schema.Void,
68
+ RpcSchema.Stream<State, Schema.Never>
69
+ >
70
+ >,
71
+ E,
72
+ Exclude<R, GenServer.SendDiscard> | Scope.Scope
73
+ > {
74
+ const { handlers } = yield* GenServer.makeHandlers(schema, layer)
75
+ const entityHandlers: Record<
76
+ string,
77
+ (request: Envelope.Request<any>) => any
78
+ > = {}
79
+ for (const { rpc, handler } of handlers.values()) {
80
+ entityHandlers[rpc._tag] = (request: Envelope.Request<any>) =>
81
+ handler({
82
+ payload: request.payload,
83
+ context: ClusterRequest.context(request),
84
+ })
85
+ }
86
+ return entityHandlers as any
87
+ })
88
+
89
+ /**
90
+ * @since 1.0.0
91
+ * @category Entity
92
+ */
93
+ export class ClusterRequest extends Context.Service<
94
+ ClusterRequest,
95
+ Envelope.Request<Rpc.AnyWithProps>
96
+ >()("effect-genserver/ClusterGenServer/ClusterRequest") {}