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.
@@ -0,0 +1,493 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ import * as Rpc from "effect/unstable/rpc/Rpc"
5
+ import * as RpcGroup from "effect/unstable/rpc/RpcGroup"
6
+ import * as Effect from "effect/Effect"
7
+ import * as Schema from "effect/Schema"
8
+ import * as Layer from "effect/Layer"
9
+ import type * as Scope from "effect/Scope"
10
+ import * as Context from "effect/Context"
11
+ import type * as Option from "effect/Option"
12
+ import * as Persistence from "effect/unstable/persistence/Persistence"
13
+ import * as PubSub from "effect/PubSub"
14
+ import * as MutableRef from "effect/MutableRef"
15
+ import * as Stream from "effect/Stream"
16
+ import * as Semaphore from "effect/Semaphore"
17
+ import { flow, identity, pipe } from "effect/Function"
18
+ import type * as RpcSchema from "effect/unstable/rpc/RpcSchema"
19
+ import * as Latch from "effect/Latch"
20
+ import type { Types } from "effect"
21
+
22
+ /**
23
+ * @since 1.0.0
24
+ * @category Models
25
+ */
26
+ export interface GenServer<State extends Schema.Top, Rpcs extends Rpc.Any> {
27
+ readonly stateSchema: State
28
+ readonly protocol: RpcGroup.RpcGroup<Rpcs>
29
+
30
+ readonly sender: Effect.Effect<
31
+ <
32
+ const Tag extends Rpcs["_tag"],
33
+ Rpc = Rpc.ExtractTag<Rpcs, Tag>,
34
+ Payload = Rpc.PayloadConstructor<Rpc>,
35
+ >(
36
+ tag: Tag,
37
+ ...args: Types.EqualsWith<
38
+ Payload,
39
+ void,
40
+ [payload?: Payload],
41
+ [payload: Payload]
42
+ >
43
+ ) => Effect.Effect<void>,
44
+ never,
45
+ SendDiscard
46
+ >
47
+
48
+ of<A extends Handlers<State, Rpcs>>(
49
+ initialState: State["Type"],
50
+ handlers: A,
51
+ ): readonly [state: State["Type"], handlers: A]
52
+
53
+ toLayer<A extends Handlers<State, Rpcs>, E, R>(
54
+ build: Effect.Effect<readonly [state: State["Type"], handlers: A], E, R>,
55
+ ): Layer.Layer<
56
+ ToHandler<Rpcs> | InitialState,
57
+ E,
58
+ Exclude<R, Scope.Scope> | HandlerServices<A>
59
+ >
60
+
61
+ toLayerPersisted<A extends Handlers<State, Rpcs>, E, R>(
62
+ build: (
63
+ getPersistedState: (
64
+ id: string,
65
+ ) => Effect.Effect<
66
+ Option.Option<State["Type"]>,
67
+ Persistence.PersistenceError,
68
+ State["DecodingServices"]
69
+ >,
70
+ ) => Effect.Effect<readonly [state: State["Type"], handlers: A], E, R>,
71
+ ): Layer.Layer<
72
+ ToHandler<Rpcs> | InitialState,
73
+ E,
74
+ | Exclude<R, Scope.Scope>
75
+ | HandlerServices<A>
76
+ | Persistence.BackingPersistence
77
+ | State["EncodingServices"]
78
+ >
79
+ }
80
+
81
+ /**
82
+ * @since 1.0.0
83
+ * @category Initial state
84
+ */
85
+ export interface InitialState {
86
+ readonly _: unique symbol
87
+ }
88
+
89
+ /**
90
+ * @since 1.0.0
91
+ * @category Handlers
92
+ */
93
+ export interface Handler<Tag extends string> {
94
+ readonly _: unique symbol
95
+ readonly _tag: Tag
96
+ }
97
+
98
+ /**
99
+ * @since 1.0.0
100
+ * @category Initial state
101
+ */
102
+ export class SendDiscard extends Context.Service<
103
+ SendDiscard,
104
+ (tag: string, payload: any) => Effect.Effect<void>
105
+ >()("@effectfultech/clanka-utils/GenServer/SendDiscard") {}
106
+
107
+ /**
108
+ * @since 1.0.0
109
+ * @category Handlers
110
+ */
111
+ export type ToHandler<Rpc extends Rpc.Any> = Rpc extends Rpc.Any
112
+ ? Handler<Rpc["_tag"]>
113
+ : never
114
+
115
+ /**
116
+ * @since 1.0.0
117
+ * @category Handlers
118
+ */
119
+ export type Handlers<State extends Schema.Top, Rpcs extends Rpc.Any> = {
120
+ [R in Rpcs as R["_tag"]]: HandlerFn<State, R, any>
121
+ }
122
+
123
+ /**
124
+ * @since 1.0.0
125
+ * @category Handlers
126
+ */
127
+ export type HandlerFn<
128
+ State extends Schema.Top,
129
+ Rpc extends Rpc.Any,
130
+ R,
131
+ > = (options: {
132
+ readonly state: State["Type"]
133
+ readonly payload: Rpc.Payload<Rpc>
134
+ readonly context: Context.Context<never>
135
+ }) => Rpc.WrapperOr<
136
+ Effect.Effect<
137
+ readonly [state: State["Type"], result: Rpc.Success<Rpc>],
138
+ Rpc.Error<Rpc>,
139
+ R
140
+ >
141
+ >
142
+
143
+ /**
144
+ * @since 1.0.0
145
+ * @category Handlers
146
+ */
147
+ export type HandlerServices<Handlers extends Record<string, any>> =
148
+ keyof Handlers extends infer K
149
+ ? K extends keyof Handlers
150
+ ? ReturnType<Handlers[K]> extends Effect.Effect<
151
+ infer _A,
152
+ infer _E,
153
+ infer _R
154
+ >
155
+ ? _R
156
+ : never
157
+ : never
158
+ : never
159
+
160
+ /**
161
+ * @since 1.0.0
162
+ * @category Constructors
163
+ */
164
+ export const make = <
165
+ State extends Schema.Top,
166
+ const Rpcs extends ReadonlyArray<Rpc.Any>,
167
+ >(options: {
168
+ readonly state: State
169
+ readonly protocol: Rpcs
170
+ }): GenServer<State, Rpcs[number]> =>
171
+ fromRpcGroup({
172
+ state: options.state,
173
+ group: RpcGroup.make(...options.protocol),
174
+ })
175
+
176
+ /**
177
+ * @since 1.0.0
178
+ * @category Constructors
179
+ */
180
+ export const fromRpcGroup = <
181
+ State extends Schema.Top,
182
+ Rpcs extends Rpc.Any,
183
+ >(options: {
184
+ readonly state: State
185
+ readonly group: RpcGroup.RpcGroup<Rpcs>
186
+ }): GenServer<State, Rpcs> => {
187
+ const self = Object.create(Proto)
188
+ self.stateSchema = options.state
189
+ self.protocol = options.group
190
+ return self
191
+ }
192
+
193
+ const Proto: Omit<GenServer<any, any>, "stateSchema" | "protocol"> = {
194
+ get sender() {
195
+ return SendDiscard.useSync((send) => send as any)
196
+ },
197
+ of(initialState: any, handlers: any) {
198
+ return [initialState, handlers]
199
+ },
200
+ toLayer(
201
+ this: GenServer<any, any>,
202
+ build: Effect.Effect<
203
+ readonly [initialState: any, Handlers<any, any>],
204
+ any,
205
+ any
206
+ >,
207
+ ) {
208
+ return Layer.fresh(
209
+ Layer.effectContext(
210
+ Effect.gen(function* () {
211
+ const services = yield* Effect.context()
212
+ const [initialState, handlers] = yield* build
213
+ const handlerMap = new Map<string, any>()
214
+ handlerMap.set(initialStateKey, initialState)
215
+ for (const [tag, handler] of Object.entries(handlers)) {
216
+ handlerMap.set(handlerKey(tag), (options: any) =>
217
+ Rpc.wrapMap(handler(options), Effect.provideContext(services)),
218
+ )
219
+ }
220
+ return Context.makeUnsafe(handlerMap)
221
+ }),
222
+ ),
223
+ )
224
+ },
225
+ toLayerPersisted(
226
+ this: GenServer<any, any>,
227
+ build: (
228
+ load: (
229
+ id: string,
230
+ ) => Effect.Effect<Option.Option<any>, Persistence.PersistenceError, any>,
231
+ ) => Effect.Effect<
232
+ readonly [initialState: any, Handlers<any, any>],
233
+ any,
234
+ any
235
+ >,
236
+ options?: {
237
+ readonly storeId?: string | undefined
238
+ },
239
+ ) {
240
+ const storeId = options?.storeId ?? "machine"
241
+ const stateFromJson = Schema.toCodecJson(this.stateSchema)
242
+ const decode = Schema.decodeUnknownEffect(stateFromJson)
243
+ const encode = Schema.encodeUnknownEffect(stateFromJson)
244
+ return Layer.fresh(
245
+ Layer.effectContext(
246
+ Effect.gen(function* () {
247
+ const services = yield* Effect.context()
248
+ const persistence = yield* Persistence.BackingPersistence
249
+ const store = yield* persistence.make(storeId)
250
+ let currentId = "default"
251
+ const load = (id: string) =>
252
+ Effect.suspend(() => {
253
+ currentId = id
254
+ return store.get(id)
255
+ }).pipe(
256
+ Effect.flatMap((a) =>
257
+ a ? Effect.asSome(Effect.orDie(decode(a))) : Effect.succeedNone,
258
+ ),
259
+ )
260
+ const [initialState, handlers] = yield* build(load)
261
+ const handlerMap = new Map<string, any>([
262
+ initialStateKey,
263
+ initialState,
264
+ ])
265
+ for (const [tag, handler] of Object.entries(handlers)) {
266
+ handlerMap.set(handlerKey(tag), (options: any) =>
267
+ Rpc.wrapMap(
268
+ handler(options),
269
+ flow(
270
+ Effect.tap(([state]) =>
271
+ Effect.flatMap(encode(state), (a) =>
272
+ store.set(currentId, a as any, undefined),
273
+ ),
274
+ ),
275
+ Effect.provideContext(services),
276
+ ),
277
+ ),
278
+ )
279
+ }
280
+ return Context.makeUnsafe(handlerMap)
281
+ }),
282
+ ),
283
+ )
284
+ },
285
+ }
286
+
287
+ const initialStateKey = `effect-genserver/GenServer/InitialState`
288
+ const handlerKey = (tag: string) => `effect-genserver/GenServer/Handler/${tag}`
289
+
290
+ /**
291
+ * @since 1.0.0
292
+ * @category Handlers
293
+ */
294
+ export const makeHandlers = Effect.fnUntraced(function* <
295
+ State extends Schema.Top,
296
+ Rpcs extends Rpc.Any,
297
+ E,
298
+ R,
299
+ >(
300
+ schema: GenServer<State, Rpcs>,
301
+ layer: Layer.Layer<ToHandler<Rpcs> | InitialState, E, R>,
302
+ ): Effect.fn.Return<
303
+ {
304
+ readonly state: MutableRef.MutableRef<State["Type"]>
305
+ readonly pubsub: PubSub.PubSub<State["Type"]>
306
+ readonly handlers: ReadonlyMap<
307
+ string,
308
+ {
309
+ readonly rpc: Rpc.AnyWithProps
310
+ readonly handler: (options: {
311
+ readonly payload: any
312
+ readonly context: Context.Context<never>
313
+ }) => Rpc.WrapperOr<Effect.Effect<any, any>>
314
+ }
315
+ >
316
+ },
317
+ E,
318
+ Exclude<R, SendDiscard> | Scope.Scope
319
+ > {
320
+ const handlers = new Map<
321
+ string,
322
+ {
323
+ readonly rpc: Rpc.AnyWithProps
324
+ readonly handler: (options: {
325
+ readonly payload: any
326
+ readonly context: Context.Context<never>
327
+ }) => Rpc.WrapperOr<Effect.Effect<any, any>>
328
+ }
329
+ >()
330
+ const scope = yield* Effect.scope
331
+ const startLatch = Latch.makeUnsafe(false)
332
+ let started = false
333
+ const sendSemaphore = Semaphore.makeUnsafe(1)
334
+ const stateChanges = RpcStateChanges(schema)
335
+
336
+ const sendDiscard = (tag: string, payload: any) => {
337
+ const eff = started
338
+ ? sendDiscardImpl(tag, payload)
339
+ : startLatch.whenOpen(sendDiscardImpl(tag, payload))
340
+ return eff.pipe(Effect.forkIn(scope), Effect.asVoid)
341
+ }
342
+ const sendDiscardImpl = (tag: string, payload: any) =>
343
+ Effect.suspend(() => {
344
+ const entry = handlers.get(tag)
345
+ if (!entry) {
346
+ return Effect.void
347
+ }
348
+ const result = entry.handler({
349
+ payload,
350
+ context: Context.empty(),
351
+ })
352
+ return Rpc.isWrapper(result) ? result.value : result
353
+ })
354
+
355
+ const services = yield* Layer.build(layer).pipe(
356
+ Effect.provideService(SendDiscard, sendDiscard),
357
+ )
358
+ const state = MutableRef.make(
359
+ services.mapUnsafe.get(initialStateKey) as State["Type"],
360
+ )
361
+ const pubsub = yield* PubSub.unbounded<State["Type"]>({
362
+ replay: 1,
363
+ })
364
+ PubSub.publishUnsafe(pubsub, state.current)
365
+
366
+ handlers.set(stateChanges._tag, {
367
+ rpc: stateChanges,
368
+ handler: (_) => Stream.fromPubSub(pubsub) as any,
369
+ })
370
+
371
+ for (const rpc of schema.protocol.requests.values()) {
372
+ const handler = services.mapUnsafe.get(handlerKey(rpc._tag)) as HandlerFn<
373
+ State,
374
+ any,
375
+ never
376
+ >
377
+ handlers.set(rpc._tag, {
378
+ rpc: rpc as any,
379
+ handler: (options) =>
380
+ Rpc.wrapMap(
381
+ handler({
382
+ state: state.current,
383
+ payload: options.payload,
384
+ context: options.context,
385
+ }),
386
+ (effect) =>
387
+ pipe(
388
+ effect,
389
+ Effect.map(([nextState, result]) => {
390
+ if (nextState !== state.current) {
391
+ MutableRef.set(state, nextState)
392
+ PubSub.publishUnsafe(pubsub, nextState)
393
+ }
394
+ return result
395
+ }),
396
+ sendSemaphore.withPermits(1),
397
+ ),
398
+ ),
399
+ })
400
+ }
401
+
402
+ startLatch.openUnsafe()
403
+ started = true
404
+
405
+ return { handlers, state, pubsub }
406
+ })
407
+
408
+ /**
409
+ * @since 1.0.0
410
+ * @category RpcServer
411
+ */
412
+ export const RpcStateChanges = <State extends Schema.Top, Rpcs extends Rpc.Any>(
413
+ server: GenServer<State, Rpcs>,
414
+ ): Rpc.Rpc<
415
+ "GenServerChanges",
416
+ Schema.Void,
417
+ RpcSchema.Stream<State, Schema.Never>
418
+ > =>
419
+ Rpc.make("GenServerChanges", {
420
+ success: server.stateSchema,
421
+ stream: true,
422
+ })
423
+
424
+ /**
425
+ * @since 1.0.0
426
+ * @category Actor
427
+ */
428
+ export interface Actor<State extends Schema.Top, Rpcs extends Rpc.Any> {
429
+ readonly changes: Stream.Stream<State["Type"]>
430
+ readonly state: MutableRef.MutableRef<State["Type"]>
431
+ send<
432
+ Tag extends Rpcs["_tag"],
433
+ Rpc = Rpc.ExtractTag<Rpcs, Tag>,
434
+ Payload = Rpc.PayloadConstructor<Rpc>,
435
+ >(
436
+ tag: Tag,
437
+ ...args: Types.EqualsWith<
438
+ Payload,
439
+ void,
440
+ [payload?: Payload],
441
+ [payload: Payload]
442
+ >
443
+ ): Effect.Effect<Rpc.Success<Rpc>, Rpc.Error<Rpc>>
444
+ }
445
+
446
+ /**
447
+ * @since 1.0.0
448
+ * @category Actor
449
+ */
450
+ export const makeActor = Effect.fnUntraced(function* <
451
+ State extends Schema.Top,
452
+ Rpcs extends Rpc.Any,
453
+ E,
454
+ R,
455
+ >(
456
+ schema: GenServer<State, Rpcs>,
457
+ layer: Layer.Layer<ToHandler<Rpcs> | InitialState, E, R>,
458
+ options?: {
459
+ readonly requestContext?: Context.Context<never> | undefined
460
+ },
461
+ ): Effect.fn.Return<
462
+ Actor<State, Rpcs>,
463
+ E,
464
+ Exclude<R, SendDiscard> | Scope.Scope
465
+ > {
466
+ const requestContext = options?.requestContext ?? Context.empty()
467
+
468
+ const { handlers, state, pubsub } = yield* makeHandlers(schema, layer)
469
+
470
+ const send = <Tag extends Rpcs["_tag"], Rpc = Rpc.ExtractTag<Rpcs, Tag>>(
471
+ tag: Tag,
472
+ payload_?: Rpc.PayloadConstructor<Rpc>,
473
+ ) => {
474
+ const entry = handlers.get(tag)
475
+ if (!entry) {
476
+ return Effect.die(`Unknown tag: ${tag}`)
477
+ }
478
+ const effectOrWrap = entry.handler({
479
+ payload:
480
+ payload_ !== undefined
481
+ ? entry.rpc.payloadSchema.make(payload_)
482
+ : undefined,
483
+ context: requestContext,
484
+ })
485
+ return Rpc.isWrapper(effectOrWrap) ? effectOrWrap.value : effectOrWrap
486
+ }
487
+
488
+ return identity<Actor<State, Rpcs>>({
489
+ changes: Stream.fromPubSub(pubsub),
490
+ state,
491
+ send: send as any,
492
+ })
493
+ })
@@ -0,0 +1,65 @@
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 Context from "effect/Context"
9
+ import type * as RpcMessage from "effect/unstable/rpc/RpcMessage"
10
+ import type * as Headers from "effect/unstable/http/Headers"
11
+ import * as GenServer from "./GenServer.ts"
12
+
13
+ /**
14
+ * @since 1.0.0
15
+ * @category RpcServer
16
+ */
17
+ export const toRpcHandlers = <
18
+ State extends Schema.Top,
19
+ Rpcs extends Rpc.Any,
20
+ E,
21
+ R,
22
+ >(
23
+ schema: GenServer.GenServer<State, Rpcs>,
24
+ layer: Layer.Layer<GenServer.ToHandler<Rpcs> | GenServer.InitialState, E, R>,
25
+ ): Layer.Layer<
26
+ Rpc.ToHandler<Rpcs> | Rpc.Handler<"GenServerChanges">,
27
+ E,
28
+ Exclude<R, GenServer.SendDiscard>
29
+ > =>
30
+ Layer.effectContext(
31
+ Effect.gen(function* () {
32
+ const { handlers } = yield* GenServer.makeHandlers(schema, layer)
33
+ const contextMap = new Map<string, Rpc.Handler<string>>()
34
+ const services = yield* Effect.context()
35
+
36
+ for (const { rpc, handler } of handlers.values()) {
37
+ contextMap.set(rpc.key, {
38
+ _: null as any,
39
+ tag: rpc._tag,
40
+ context: services,
41
+ handler: (payload, options) =>
42
+ handler({
43
+ payload,
44
+ context: RpcContext.context(options as any),
45
+ }) as any,
46
+ })
47
+ }
48
+
49
+ return Context.makeUnsafe(contextMap)
50
+ }),
51
+ )
52
+
53
+ /**
54
+ * @since 1.0.0
55
+ * @category RpcServer
56
+ */
57
+ export class RpcContext extends Context.Service<
58
+ RpcContext,
59
+ {
60
+ readonly client: Rpc.ServerClient
61
+ readonly requestId: RpcMessage.RequestId
62
+ readonly headers: Headers.Headers
63
+ readonly rpc: Rpc.AnyWithProps
64
+ }
65
+ >()("effect-genserver/RpcGenServer/RpcContext") {}
package/src/index.ts ADDED
@@ -0,0 +1,19 @@
1
+ /**
2
+ * @since 1.0.0
3
+ */
4
+ export * as AtomGenServer from "./AtomGenServer.ts"
5
+
6
+ /**
7
+ * @since 1.0.0
8
+ */
9
+ export * as ClusterGenServer from "./ClusterGenServer.ts"
10
+
11
+ /**
12
+ * @since 1.0.0
13
+ */
14
+ export * as GenServer from "./GenServer.ts"
15
+
16
+ /**
17
+ * @since 1.0.0
18
+ */
19
+ export * as RpcGenServer from "./RpcGenServer.ts"