@effect-app/infra 1.38.0 → 1.40.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.
@@ -177,21 +177,32 @@ export interface ExtendedMiddleware<Context, CTXMap extends Record<string, RPCCo
177
177
  }
178
178
 
179
179
  export const RouterSymbol = Symbol()
180
- export interface RouterS<Rsc> {
180
+ export interface RouterShape<Rsc> {
181
181
  [RouterSymbol]: Rsc
182
182
  }
183
183
 
184
+ type RPCRouteR<T extends Rpc.Rpc<any, any>> = [T] extends [
185
+ Rpc.Rpc<any, infer R>
186
+ ] ? R
187
+ : never
188
+
189
+ type RPCRouteReq<T extends Rpc.Rpc<any, any>> = [T] extends [
190
+ Rpc.Rpc<infer Req, any>
191
+ ] ? Req
192
+ : never
193
+
184
194
  export const makeRouter = <Context, CTXMap extends Record<string, RPCContextMap.Any>>(
185
195
  middleware: ExtendedMiddleware<Context, CTXMap>,
186
196
  devMode: boolean
187
197
  ) => {
188
198
  const rpc = makeRpc(middleware)
189
- function matchFor<Rsc extends Record<string, any> & { meta: { moduleName: string } }>(
190
- rsc: Rsc
199
+ function matchFor<
200
+ const ModuleName extends string,
201
+ const Rsc extends Record<string, any>
202
+ >(
203
+ rsc: Rsc & { meta: { moduleName: ModuleName } }
191
204
  ) {
192
- const meta = (rsc as any).meta as { moduleName: string }
193
- if (!meta) throw new Error("Resource has no meta specified") // TODO: do something with moduleName+cur etc.
194
-
205
+ const meta = rsc.meta
195
206
  type Filtered = Filter<Rsc>
196
207
  const filtered = typedKeysOf(rsc).reduce((acc, cur) => {
197
208
  if (Predicate.isObject(rsc[cur]) && rsc[cur]["success"]) {
@@ -388,25 +399,15 @@ export const makeRouter = <Context, CTXMap extends Record<string, RPCContextMap.
388
399
  >
389
400
  }
390
401
 
391
- type RPCRouteR<T extends Rpc.Rpc<any, any>> = [T] extends [
392
- Rpc.Rpc<any, infer R>
393
- ] ? R
394
- : never
395
-
396
- type RPCRouteReq<T extends Rpc.Rpc<any, any>> = [T] extends [
397
- Rpc.Rpc<infer Req, any>
398
- ] ? Req
399
- : never
400
-
401
402
  const rpcRouter = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
402
403
  RPCRouteReq<typeof mapped[keyof typeof mapped]>,
403
404
  RPCRouteR<typeof mapped[keyof typeof mapped]>
404
405
  >
405
406
 
406
- type Router = RouterS<Rsc>
407
+ type Router = RouterShape<Rsc>
407
408
  const r: HttpRouter.HttpRouter.TagClass<
408
409
  Router,
409
- string,
410
+ `${typeof meta.moduleName}Router`,
410
411
  never,
411
412
  Exclude<
412
413
  RPCRouteR<
@@ -414,7 +415,7 @@ export const makeRouter = <Context, CTXMap extends Record<string, RPCContextMap.
414
415
  >,
415
416
  { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]
416
417
  >
417
- > = (class Router extends HttpRouter.Tag(meta.moduleName + "Router")<Router>() {}) as any
418
+ > = (class Router extends HttpRouter.Tag(`${meta.moduleName}Router`)<Router>() {}) as any
418
419
 
419
420
  const layer = r.use((router) =>
420
421
  Effect.gen(function*() {
@@ -456,7 +457,165 @@ export const makeRouter = <Context, CTXMap extends Record<string, RPCContextMap.
456
457
  }
457
458
  }
458
459
 
460
+ const effect = <
461
+ E,
462
+ R,
463
+ THandlers extends {
464
+ // import to keep them separate via | for type checking!!
465
+ [K in Keys]: AHandler<Rsc[K]>
466
+ },
467
+ TLayers extends NonEmptyArray<Layer.Layer.Any>
468
+ >(
469
+ layers: TLayers,
470
+ make: Effect<THandlers, E, R>
471
+ ) => {
472
+ type Router = RouterShape<Rsc>
473
+ const r: HttpRouter.HttpRouter.TagClass<
474
+ Router,
475
+ `${typeof meta.moduleName}Router`,
476
+ never,
477
+ Exclude<
478
+ RPCRouteR<
479
+ { [K in keyof Filter<Rsc>]: Rpc.Rpc<Rsc[K], _R<ReturnType<THandlers[K]["handler"]>>> }[keyof Filter<Rsc>]
480
+ >,
481
+ { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]
482
+ >
483
+ > = (class Router extends HttpRouter.Tag(`${meta.moduleName}Router`)<Router>() {}) as any
484
+
485
+ const layer = r.use((router) =>
486
+ Effect.gen(function*() {
487
+ const controllers = yield* make
488
+ // return make.pipe(Effect.map((c) => controllers(c, layers)))
489
+ const mapped = typedKeysOf(filtered).reduce((acc, cur) => {
490
+ const handler = controllers[cur as keyof typeof controllers]
491
+ const req = rsc[cur]
492
+
493
+ acc[cur] = rpc.effect(
494
+ handler._tag === "raw"
495
+ ? class extends (req as any) {
496
+ static success = S.encodedSchema(req.success)
497
+ get [Serializable.symbol]() {
498
+ return this.constructor
499
+ }
500
+ get [Serializable.symbolResult]() {
501
+ return {
502
+ failure: req.failure,
503
+ success: S.encodedSchema(req.success)
504
+ }
505
+ }
506
+ } as any
507
+ : req,
508
+ (req) =>
509
+ Effect
510
+ .annotateCurrentSpan(
511
+ "requestInput",
512
+ Object.entries(req).reduce((prev, [key, value]: [string, unknown]) => {
513
+ prev[key] = key === "password"
514
+ ? "<redacted>"
515
+ : typeof value === "string" || typeof value === "number" || typeof value === "boolean"
516
+ ? typeof value === "string" && value.length > 256
517
+ ? (value.substring(0, 253) + "...")
518
+ : value
519
+ : Array.isArray(value)
520
+ ? `Array[${value.length}]`
521
+ : value === null || value === undefined
522
+ ? `${value}`
523
+ : typeof value === "object" && value
524
+ ? `Object[${Object.keys(value).length}]`
525
+ : typeof value
526
+ return prev
527
+ }, {} as Record<string, string | number | boolean>)
528
+ )
529
+ .pipe(
530
+ // can't use andThen due to some being a function and effect
531
+ Effect.zipRight(handler.handler(req as any) as any),
532
+ Effect.tapErrorCause((cause) => Cause.isFailure(cause) ? logRequestError(cause) : Effect.void),
533
+ Effect.tapDefect((cause) =>
534
+ Effect
535
+ .all([
536
+ reportRequestError(cause, {
537
+ action: `${meta.moduleName}.${req._tag}`
538
+ }),
539
+ Rpc.currentHeaders.pipe(Effect.andThen((headers) => {
540
+ return InfraLogger
541
+ .logError("Finished request", cause)
542
+ .pipe(Effect.annotateLogs({
543
+ action: `${meta.moduleName}.${req._tag}`,
544
+ req: pretty(req),
545
+ headers: pretty(headers)
546
+ // resHeaders: pretty(
547
+ // Object
548
+ // .entries(headers)
549
+ // .reduce((prev, [key, value]) => {
550
+ // prev[key] = value && typeof value === "string" ? snipString(value) : value
551
+ // return prev
552
+ // }, {} as Record<string, any>)
553
+ // )
554
+ }))
555
+ }))
556
+ ])
557
+ ),
558
+ devMode ? (_) => _ : Effect.catchAllDefect(() => Effect.die("Internal Server Error")),
559
+ Effect.withSpan("Request." + meta.moduleName + "." + req._tag, {
560
+ captureStackTrace: () => handler.stack
561
+ })
562
+ ),
563
+ meta.moduleName
564
+ ) // TODO
565
+ return acc
566
+ }, {} as any) as {
567
+ [K in Keys]: Rpc.Rpc<
568
+ Rsc[K],
569
+ _R<ReturnType<THandlers[K]["handler"]>>
570
+ >
571
+ }
572
+
573
+ const rpcRouter = RpcRouter.make(...Object.values(mapped) as any) as RpcRouter.RpcRouter<
574
+ RPCRouteReq<typeof mapped[keyof typeof mapped]>,
575
+ RPCRouteR<typeof mapped[keyof typeof mapped]>
576
+ >
577
+ const httpApp = toHttpApp(rpcRouter, {
578
+ spanPrefix: rsc
579
+ .meta
580
+ .moduleName + "."
581
+ })
582
+ const services = (yield* Effect.context<never>()).pipe(
583
+ Context.omit(Scope.Scope as never),
584
+ Context.omit(Tracer.ParentSpan as never)
585
+ )
586
+ yield* router
587
+ .all(
588
+ "/",
589
+ (httpApp
590
+ .pipe(HttpMiddleware.make(Effect.provide(services)))) as any,
591
+ // TODO: not queries.
592
+ { uninterruptible: true }
593
+ )
594
+ })
595
+ )
596
+
597
+ // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
598
+ const routes = layer.pipe(
599
+ Layer.provideMerge(r.Live),
600
+ layers ? Layer.provide(layers) as any : (_) => _
601
+ ) as Layer.Layer<
602
+ Router,
603
+ { [k in keyof TLayers]: Layer.Layer.Error<TLayers[k]> }[number] | E,
604
+ | { [k in keyof TLayers]: Layer.Layer.Context<TLayers[k]> }[number]
605
+ | Exclude<R, { [k in keyof TLayers]: Layer.Layer.Success<TLayers[k]> }[number]>
606
+ >
607
+
608
+ // Effect.Effect<HttpRouter.HttpRouter<unknown, HttpRouter.HttpRouter.DefaultServices>, never, UserRouter>
609
+
610
+ return {
611
+ moduleName: meta.moduleName,
612
+ Router: r,
613
+ routes
614
+ }
615
+ }
616
+
459
617
  const r = {
618
+ effect,
460
619
  controllers,
461
620
  ...typedKeysOf(filtered).reduce(
462
621
  (prev, cur) => {
@@ -1,6 +1,6 @@
1
- import { annotateLogscoped, flatMap } from "@effect-app/core/Effect"
1
+ import { annotateLogscoped } from "@effect-app/core/Effect"
2
2
  import { dual, pipe } from "@effect-app/core/Function"
3
- import type { RequestFiberSet } from "@effect-app/infra-adapters/RequestFiberSet"
3
+ import { RequestFiberSet } from "@effect-app/infra-adapters/RequestFiberSet"
4
4
  import { reportError } from "@effect-app/infra/errorReporter"
5
5
  import { NonEmptyString2k } from "@effect-app/schema"
6
6
  import { subHours } from "date-fns"
@@ -19,8 +19,19 @@ const reportAppError = reportError("Operations.Cleanup")
19
19
 
20
20
  const make = Effect.gen(function*() {
21
21
  const repo = yield* OperationsRepo
22
+ const reqFiberSet = yield* RequestFiberSet
22
23
  const makeOp = Effect.sync(() => OperationId.make())
23
24
 
25
+ const register = (title: NonEmptyString2k) =>
26
+ Effect.tap(
27
+ makeOp,
28
+ (id) =>
29
+ Effect.andThen(
30
+ annotateLogscoped("operationId", id),
31
+ Effect.acquireRelease(addOp(id, title), (_, exit) => finishOp(id, exit))
32
+ )
33
+ )
34
+
24
35
  const cleanup = Effect.sync(() => subHours(new Date(), 1)).pipe(
25
36
  Effect.andThen((before) => repo.query(where("updatedAt", "lt", before.toISOString()))),
26
37
  Effect.andThen((ops) => pipe(ops, batch(100, Effect.succeed, (items) => repo.removeAndPublish(items)))),
@@ -68,18 +79,75 @@ const make = Effect.gen(function*() {
68
79
  (_) => repo.save(copy(_, { updatedAt: new Date(), progress })).pipe(Effect.orDie)
69
80
  )
70
81
  }
82
+
83
+ function fork<R, R2, E, E2, A, A2>(
84
+ self: (id: OperationId) => Effect<A, E, R>,
85
+ fnc: (id: OperationId) => Effect<A2, E2, R2>,
86
+ title: NonEmptyString2k
87
+ ): Effect<
88
+ RunningOperation<A, E>,
89
+ never,
90
+ Exclude<R, Scope.Scope> | Exclude<R2, Scope.Scope>
91
+ > {
92
+ return Effect
93
+ .flatMap(
94
+ Scope.make(),
95
+ (scope) =>
96
+ register(title)
97
+ .pipe(
98
+ Scope.extend(scope),
99
+ Effect.flatMap((id) =>
100
+ forkDaemonReportRequestUnexpected(Scope.use(
101
+ self(id).pipe(Effect.withSpan(title)),
102
+ scope
103
+ ))
104
+ .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
105
+ ),
106
+ Effect.tap(({ id }) =>
107
+ Effect.interruptible(fnc(id)).pipe(
108
+ Effect.forkScoped,
109
+ Scope.extend(scope)
110
+ )
111
+ )
112
+ )
113
+ )
114
+ .pipe(Effect.provideService(RequestFiberSet, reqFiberSet))
115
+ }
116
+
117
+ const fork2: {
118
+ (title: NonEmptyString2k): <R, E, A>(
119
+ self: (opId: OperationId) => Effect<A, E, R>
120
+ ) => Effect<RunningOperation<A, E>, never, Exclude<R, Scope.Scope>>
121
+ <R, E, A>(
122
+ self: (opId: OperationId) => Effect<A, E, R>,
123
+ title: NonEmptyString2k
124
+ ): Effect<RunningOperation<A, E>, never, Exclude<R, Scope.Scope>>
125
+ } = dual(
126
+ 2,
127
+ <R, E, A>(self: (opId: OperationId) => Effect<A, E, R>, title: NonEmptyString2k) =>
128
+ Effect.flatMap(
129
+ Scope.make(),
130
+ (scope) =>
131
+ register(title)
132
+ .pipe(
133
+ Scope.extend(scope),
134
+ Effect
135
+ .flatMap((id) =>
136
+ forkDaemonReportRequestUnexpected(Scope.use(
137
+ self(id).pipe(Effect.withSpan(title)),
138
+ scope
139
+ ))
140
+ .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
141
+ )
142
+ )
143
+ )
144
+ )
145
+
71
146
  return {
72
147
  cleanup,
73
- register: (title: NonEmptyString2k) =>
74
- Effect.tap(
75
- makeOp,
76
- (id) =>
77
- Effect.andThen(
78
- annotateLogscoped("operationId", id),
79
- Effect.acquireRelease(addOp(id, title), (_, exit) => finishOp(id, exit))
80
- )
81
- ),
82
-
148
+ register,
149
+ fork,
150
+ fork2,
83
151
  all: repo.all,
84
152
  find: findOp,
85
153
  update
@@ -106,7 +174,7 @@ export class Operations extends Context.TagMakeId("effect-app/Operations", make)
106
174
  )
107
175
  .pipe(Layer.effectDiscard, Layer.provide(MainFiberSet.Live))
108
176
 
109
- static readonly Live = this.CleanupLive.pipe(Layer.provideMerge(this.toLayer()))
177
+ static readonly Live = this.CleanupLive.pipe(Layer.provideMerge(this.toLayer()), Layer.provide(RequestFiberSet.Live))
110
178
  }
111
179
 
112
180
  export interface RunningOperation<A, E> {
@@ -147,68 +215,3 @@ export const forkOperation: {
147
215
  export function forkOperationFunction<R, E, A, Inp>(fnc: (inp: Inp) => Effect<A, E, R>, title: NonEmptyString2k) {
148
216
  return (inp: Inp) => fnc(inp).pipe((_) => forkOperation(_, title))
149
217
  }
150
-
151
- export const forkOperation2: {
152
- (title: NonEmptyString2k): <R, E, A>(
153
- self: (opId: OperationId) => Effect<A, E, R>
154
- ) => Effect<RunningOperation<A, E>, never, Operations | Exclude<R, Scope.Scope>>
155
- <R, E, A>(
156
- self: (opId: OperationId) => Effect<A, E, R>,
157
- title: NonEmptyString2k
158
- ): Effect<RunningOperation<A, E>, never, Operations | Exclude<R, Scope.Scope>>
159
- } = dual(
160
- 2,
161
- <R, E, A>(self: (opId: OperationId) => Effect<A, E, R>, title: NonEmptyString2k) =>
162
- flatMap(Operations, (Operations) =>
163
- Effect.flatMap(
164
- Scope.make(),
165
- (scope) =>
166
- Operations
167
- .register(title)
168
- .pipe(
169
- Scope.extend(scope),
170
- Effect
171
- .flatMap((id) =>
172
- forkDaemonReportRequestUnexpected(Scope.use(
173
- self(id).pipe(Effect.withSpan(title)),
174
- scope
175
- ))
176
- .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
177
- )
178
- )
179
- ))
180
- )
181
-
182
- export function forkOperationWithEffect<R, R2, E, E2, A, A2>(
183
- self: (id: OperationId) => Effect<A, E, R>,
184
- fnc: (id: OperationId) => Effect<A2, E2, R2>,
185
- title: NonEmptyString2k
186
- ): Effect<
187
- RunningOperation<A, E>,
188
- never,
189
- Operations | RequestFiberSet | Exclude<R, Scope.Scope> | Exclude<R2, Scope.Scope>
190
- > {
191
- return Effect.flatMap(Operations, (Operations) =>
192
- Effect.flatMap(
193
- Scope.make(),
194
- (scope) =>
195
- Operations
196
- .register(title)
197
- .pipe(
198
- Scope.extend(scope),
199
- Effect.flatMap((id) =>
200
- forkDaemonReportRequestUnexpected(Scope.use(
201
- self(id).pipe(Effect.withSpan(title)),
202
- scope
203
- ))
204
- .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
205
- ),
206
- Effect.tap(({ id }) =>
207
- Effect.interruptible(fnc(id)).pipe(
208
- Effect.forkScoped,
209
- Scope.extend(scope)
210
- )
211
- )
212
- )
213
- ))
214
- }