@effect-app/infra 1.40.0 → 1.41.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,70 @@
1
+ import { Context, Effect, Fiber, FiberSet, Layer } from "@effect-app/core"
2
+ import type {} from "effect/Scope"
3
+ import type {} from "effect/Context"
4
+ import { InfraLogger } from "../logger.js"
5
+ import { reportNonInterruptedFailureCause } from "./QueueMaker/errors.js"
6
+ import { setRootParentSpan } from "./RequestFiberSet.js"
7
+
8
+ const make = Effect.gen(function*() {
9
+ const set = yield* FiberSet.make<unknown, never>()
10
+ const add = (...fibers: Fiber.RuntimeFiber<never, never>[]) =>
11
+ Effect.sync(() => fibers.forEach((_) => FiberSet.unsafeAdd(set, _)))
12
+ const addAll = (fibers: readonly Fiber.RuntimeFiber<never, never>[]) =>
13
+ Effect.sync(() => fibers.forEach((_) => FiberSet.unsafeAdd(set, _)))
14
+ const join = FiberSet.size(set).pipe(
15
+ Effect.andThen((count) => InfraLogger.logDebug(`Joining ${count} current fibers on the MainFiberSet`)),
16
+ Effect.andThen(FiberSet.join(set))
17
+ )
18
+ const run = FiberSet.run(set)
19
+
20
+ // const waitUntilEmpty = Effect.gen(function*() {
21
+ // const currentSize = yield* FiberSet.size(set)
22
+ // if (currentSize === 0) {
23
+ // return
24
+ // }
25
+ // yield* InfraLogger.logInfo("Waiting MainFiberSet to be empty: " + currentSize)
26
+ // while ((yield* FiberSet.size(set)) > 0) yield* Effect.sleep("250 millis")
27
+ // yield* InfraLogger.logDebug("MainFiberSet is empty")
28
+ // })
29
+
30
+ // TODO: loop and interrupt all fibers in the set continuously?
31
+ const interrupt = Fiber.interruptAll(set)
32
+
33
+ /**
34
+ * Forks the effect into a new fiber attached to the MainFiberSet scope. Because the
35
+ * new fiber isn't attached to the parent, when the fiber executing the
36
+ * returned effect terminates, the forked fiber will continue running.
37
+ * The fiber will be interrupted when the MainFiberSet scope is closed.
38
+ *
39
+ * The parent span is set to the root span of the current fiber.
40
+ * Reports and then swallows errors.
41
+ */
42
+ function fork<A, E, R>(self: Effect<A, E, R>) {
43
+ return self.pipe(
44
+ Effect.asVoid,
45
+ Effect.catchAllCause(reportNonInterruptedFailureCause({})),
46
+ setRootParentSpan,
47
+ Effect.uninterruptible,
48
+ run
49
+ )
50
+ }
51
+ return {
52
+ interrupt,
53
+ join,
54
+ fork,
55
+ run,
56
+ add,
57
+ addAll
58
+ }
59
+ })
60
+
61
+ /**
62
+ * Whenever you fork long running (e.g worker) fibers via e.g `Effect.forkScoped` or `Effect.forkDaemon`
63
+ * you should register these long running fibers in a FiberSet, and join them at the end of your main program.
64
+ * This way any errors will blow up the main program instead of fibers dying unknowingly.
65
+ */
66
+ export class MainFiberSet extends Context.TagMakeId("MainFiberSet", make)<MainFiberSet>() {
67
+ static readonly Live = this.toLayerScoped()
68
+ static readonly JoinLive = this.pipe(Effect.andThen((_) => _.join), Layer.effectDiscard, Layer.provide(this.Live))
69
+ static readonly run = <A, R>(self: Effect<A, never, R>) => this.use((_) => _.run(self))
70
+ }
@@ -1,6 +1,5 @@
1
1
  import { annotateLogscoped } from "@effect-app/core/Effect"
2
2
  import { dual, pipe } from "@effect-app/core/Function"
3
- import { RequestFiberSet } from "@effect-app/infra-adapters/RequestFiberSet"
4
3
  import { reportError } from "@effect-app/infra/errorReporter"
5
4
  import { NonEmptyString2k } from "@effect-app/schema"
6
5
  import { subHours } from "date-fns"
@@ -8,12 +7,12 @@ import type { Fiber } from "effect-app"
8
7
  import { Cause, Context, copy, Duration, Effect, Exit, Layer, Option, S, Schedule } from "effect-app"
9
8
  import type { OperationProgress } from "effect-app/Operations"
10
9
  import { Failure, Operation, OperationId, Success } from "effect-app/Operations"
11
- import { MainFiberSet } from "effect-app/services/MainFiberSet"
12
10
  import * as Scope from "effect/Scope"
13
- import { forkDaemonReportRequestUnexpected } from "../api/reportError.js"
14
11
  import { batch } from "../rateLimit.js"
12
+ import { MainFiberSet } from "./MainFiberSet.js"
15
13
  import { OperationsRepo } from "./OperationsRepo.js"
16
14
  import { where } from "./query.js"
15
+ import { RequestFiberSet } from "./RequestFiberSet.js"
17
16
 
18
17
  const reportAppError = reportError("Operations.Cleanup")
19
18
 
@@ -97,10 +96,11 @@ const make = Effect.gen(function*() {
97
96
  .pipe(
98
97
  Scope.extend(scope),
99
98
  Effect.flatMap((id) =>
100
- forkDaemonReportRequestUnexpected(Scope.use(
101
- self(id).pipe(Effect.withSpan(title)),
102
- scope
103
- ))
99
+ reqFiberSet
100
+ .forkDaemonReportRequestUnexpected(Scope.use(
101
+ self(id).pipe(Effect.withSpan(title)),
102
+ scope
103
+ ))
104
104
  .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
105
105
  ),
106
106
  Effect.tap(({ id }) =>
@@ -133,21 +133,58 @@ const make = Effect.gen(function*() {
133
133
  Scope.extend(scope),
134
134
  Effect
135
135
  .flatMap((id) =>
136
- forkDaemonReportRequestUnexpected(Scope.use(
137
- self(id).pipe(Effect.withSpan(title)),
138
- scope
139
- ))
136
+ reqFiberSet
137
+ .forkDaemonReportRequestUnexpected(Scope.use(
138
+ self(id).pipe(Effect.withSpan(title)),
139
+ scope
140
+ ))
140
141
  .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
141
142
  )
142
143
  )
143
144
  )
144
145
  )
145
146
 
147
+ const forkOperation: {
148
+ (title: NonEmptyString2k): <R, E, A>(
149
+ self: Effect<A, E, R>
150
+ ) => Effect<RunningOperation<A, E>, never, Exclude<R, Scope.Scope>>
151
+ <R, E, A>(
152
+ self: Effect<A, E, R>,
153
+ title: NonEmptyString2k
154
+ ): Effect<RunningOperation<A, E>, never, Exclude<R, Scope.Scope>>
155
+ } = dual(
156
+ 2,
157
+ <R, E, A>(self: Effect<A, E, R>, title: NonEmptyString2k) =>
158
+ Effect.flatMap(
159
+ Scope.make(),
160
+ (scope) =>
161
+ register(title)
162
+ .pipe(
163
+ Scope.extend(scope),
164
+ Effect
165
+ .flatMap((id) =>
166
+ reqFiberSet
167
+ .forkDaemonReportRequestUnexpected(Scope.use(
168
+ self.pipe(Effect.withSpan(title)),
169
+ scope
170
+ ))
171
+ .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
172
+ )
173
+ )
174
+ )
175
+ )
176
+
177
+ function forkOperationFunction<R, E, A, Inp>(fnc: (inp: Inp) => Effect<A, E, R>, title: NonEmptyString2k) {
178
+ return (inp: Inp) => fnc(inp).pipe((_) => forkOperation(_, title))
179
+ }
180
+
146
181
  return {
147
182
  cleanup,
148
183
  register,
149
184
  fork,
150
185
  fork2,
186
+ forkOperation,
187
+ forkOperationFunction,
151
188
  all: repo.all,
152
189
  find: findOp,
153
190
  update
@@ -181,37 +218,3 @@ export interface RunningOperation<A, E> {
181
218
  id: OperationId
182
219
  fiber: Fiber.RuntimeFiber<A, E>
183
220
  }
184
-
185
- export const forkOperation: {
186
- (title: NonEmptyString2k): <R, E, A>(
187
- self: Effect<A, E, R>
188
- ) => Effect<RunningOperation<A, E>, never, Operations | Exclude<R, Scope.Scope>>
189
- <R, E, A>(
190
- self: Effect<A, E, R>,
191
- title: NonEmptyString2k
192
- ): Effect<RunningOperation<A, E>, never, Operations | Exclude<R, Scope.Scope>>
193
- } = dual(
194
- 2,
195
- <R, E, A>(self: Effect<A, E, R>, title: NonEmptyString2k) =>
196
- Effect.flatMap(
197
- Scope.make(),
198
- (scope) =>
199
- Operations
200
- .register(title)
201
- .pipe(
202
- Scope.extend(scope),
203
- Effect
204
- .flatMap((id) =>
205
- forkDaemonReportRequestUnexpected(Scope.use(
206
- self.pipe(Effect.withSpan(title)),
207
- scope
208
- ))
209
- .pipe(Effect.map((fiber): RunningOperation<A, E> => ({ fiber, id })))
210
- )
211
- )
212
- )
213
- )
214
-
215
- export function forkOperationFunction<R, E, A, Inp>(fnc: (inp: Inp) => Effect<A, E, R>, title: NonEmptyString2k) {
216
- return (inp: Inp) => fnc(inp).pipe((_) => forkOperation(_, title))
217
- }
@@ -1,38 +1,11 @@
1
- import { setRootParentSpan } from "@effect-app/infra-adapters/RequestFiberSet"
2
1
  import { reportError } from "@effect-app/infra/errorReporter"
3
2
  import { Cause, Effect, Exit } from "effect-app"
4
- import { MainFiberSet } from "effect-app/services/MainFiberSet"
5
3
 
6
4
  const reportQueueError_ = reportError("Queue")
7
5
 
8
6
  export const reportQueueError = <E>(cause: Cause<E>, extras?: Record<string, unknown>) =>
9
7
  reportQueueError_(cause, extras)
10
8
 
11
- /**
12
- * Forks the effect into a new fiber attached to the MainFiberSet scope. Because the
13
- * new fiber isn't attached to the parent, when the fiber executing the
14
- * returned effect terminates, the forked fiber will continue running.
15
- * The fiber will be interrupted when the MainFiberSet scope is closed.
16
- *
17
- * The parent span is set to the root span of the current fiber.
18
- * Reports and then swallows errors.
19
- *
20
- * @tsplus getter effect/io/Effect forkDaemonReportQueue
21
- */
22
- export function forkDaemonReportQueue<A, E, R>(self: Effect<A, E, R>) {
23
- return self.pipe(
24
- Effect.asVoid,
25
- Effect.catchAllCause(reportNonInterruptedFailureCause({})),
26
- setRootParentSpan,
27
- Effect.uninterruptible,
28
- MainFiberSet.run
29
- )
30
- }
31
-
32
- export const reportFatalQueueError = reportError(
33
- "FatalQueue"
34
- )
35
-
36
9
  export function reportNonInterruptedFailure(context?: Record<string, unknown>) {
37
10
  const report = reportNonInterruptedFailureCause(context)
38
11
  return <A, E, R>(inp: Effect<A, E, R>): Effect<Exit<A, E>, never, R> =>
@@ -0,0 +1,104 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import type { Tracer } from "@effect-app/core"
3
+ import { Context, Effect, Fiber, FiberSet, Option } from "@effect-app/core"
4
+ import { reportRequestError, reportUnknownRequestError } from "../api/reportError.js"
5
+ import { InfraLogger } from "../logger.js"
6
+
7
+ const getRootParentSpan = Effect.gen(function*() {
8
+ let span: Tracer.AnySpan | null = yield* Effect.currentSpan.pipe(
9
+ Effect.catchTag("NoSuchElementException", () => Effect.succeed(null))
10
+ )
11
+ if (!span) return span
12
+ while (span._tag === "Span" && Option.isSome(span.parent)) {
13
+ span = span.parent.value
14
+ }
15
+ return span
16
+ })
17
+
18
+ export const setRootParentSpan = <A, E, R>(self: Effect<A, E, R>) =>
19
+ getRootParentSpan.pipe(Effect.andThen((span) => span ? Effect.withParentSpan(self, span) : self))
20
+
21
+ const make = Effect.gen(function*() {
22
+ const set = yield* FiberSet.make<any, any>()
23
+ const add = (...fibers: Fiber.RuntimeFiber<any, any>[]) =>
24
+ Effect.sync(() => fibers.forEach((_) => FiberSet.unsafeAdd(set, _)))
25
+ const addAll = (fibers: readonly Fiber.RuntimeFiber<any, any>[]) =>
26
+ Effect.sync(() => fibers.forEach((_) => FiberSet.unsafeAdd(set, _)))
27
+ const join = FiberSet.size(set).pipe(
28
+ Effect.andThen((count) => InfraLogger.logInfo(`Joining ${count} current fibers on the RequestFiberSet`)),
29
+ Effect.andThen(FiberSet.join(set))
30
+ )
31
+ const run = FiberSet.run(set)
32
+ const register = <A, E, R>(self: Effect<A, E, R>) =>
33
+ self.pipe(Effect.fork, Effect.tap(add), Effect.andThen(Fiber.join))
34
+
35
+ // const waitUntilEmpty = Effect.gen(function*() {
36
+ // const currentSize = yield* FiberSet.size(set)
37
+ // if (currentSize === 0) {
38
+ // return
39
+ // }
40
+ // yield* Effect.logInfo("Waiting RequestFiberSet to be empty: " + currentSize)
41
+ // while ((yield* FiberSet.size(set)) > 0) yield* Effect.sleep("250 millis")
42
+ // yield* Effect.logDebug("RequestFiberSet is empty")
43
+ // })
44
+ // TODO: loop and interrupt all fibers in the set continuously?
45
+ const interrupt = Fiber.interruptAll(set)
46
+
47
+ /**
48
+ * Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
49
+ * new fiber isn't attached to the parent, when the fiber executing the
50
+ * returned effect terminates, the forked fiber will continue running.
51
+ * The fiber will be interrupted when the RequestFiberSet scope is closed.
52
+ *
53
+ * The parent span is set to the root span of the current fiber.
54
+ * Reports errors.
55
+ */
56
+ function forkDaemonReportRequest<R, E, A>(self: Effect<A, E, R>) {
57
+ return self.pipe(
58
+ reportRequestError,
59
+ setRootParentSpan,
60
+ Effect.uninterruptible,
61
+ run
62
+ )
63
+ }
64
+
65
+ /**
66
+ * Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
67
+ * new fiber isn't attached to the parent, when the fiber executing the
68
+ * returned effect terminates, the forked fiber will continue running.
69
+ * The fiber will be interrupted when the RequestFiberSet scope is closed.
70
+ *
71
+ * The parent span is set to the root span of the current fiber.
72
+ * Reports unexpected errors.
73
+ */
74
+ function forkDaemonReportRequestUnexpected<R, E, A>(self: Effect<A, E, R>) {
75
+ return self
76
+ .pipe(
77
+ reportUnknownRequestError,
78
+ setRootParentSpan,
79
+ Effect.uninterruptible,
80
+ run
81
+ )
82
+ }
83
+
84
+ return {
85
+ interrupt,
86
+ join,
87
+ run,
88
+ add,
89
+ addAll,
90
+ register,
91
+ forkDaemonReportRequest,
92
+ forkDaemonReportRequestUnexpected
93
+ }
94
+ })
95
+
96
+ /**
97
+ * Whenever you fork a fiber for a Request, and you want to prevent dependent services to close prematurely on interruption,
98
+ * like the ServiceBus Sender, you should register these fibers in this FiberSet.
99
+ */
100
+ export class RequestFiberSet extends Context.TagMakeId("RequestFiberSet", make)<RequestFiberSet>() {
101
+ static readonly Live = this.toLayerScoped()
102
+ static readonly register = <A, E, R>(self: Effect<A, E, R>) => this.use((_) => _.register(self))
103
+ static readonly run = <A, E, R>(self: Effect<A, E, R>) => this.use((_) => _.run(self))
104
+ }