@effect-app/infra 4.0.0-beta.281 → 4.0.0-beta.283
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/CHANGELOG.md +37 -0
- package/dist/ClusterCosmos.d.ts +15 -15
- package/dist/MainFiberSet.d.ts +1 -0
- package/dist/MainFiberSet.d.ts.map +1 -1
- package/dist/MainFiberSet.js +14 -2
- package/dist/logger.d.ts +4 -4
- package/dist/routing/utils.js +2 -2
- package/dist/routing.d.ts.map +1 -1
- package/dist/routing.js +34 -12
- package/package.json +5 -5
- package/src/MainFiberSet.ts +18 -1
- package/src/routing/utils.ts +1 -1
- package/src/routing.ts +73 -20
- package/test/repository-ext.test.ts +27 -0
- package/test/rpc-e2e-invalidation.test.ts +167 -3
- package/src/QueueMaker/SQLQueue.ts +0 -186
- package/src/QueueMaker/memQueue.ts +0 -136
- package/src/QueueMaker/sbqueue.ts +0 -125
- package/src/RequestFiberSet.ts +0 -110
- package/src/ServiceBus.ts +0 -219
- package/src/memQueue.ts +0 -22
|
@@ -1,136 +0,0 @@
|
|
|
1
|
-
import type { NonEmptyReadonlyArray } from "effect-app/Array"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import { QueueMeta } from "effect-app/QueueMaker"
|
|
4
|
-
import * as S from "effect-app/Schema"
|
|
5
|
-
import { getRequestContext, setupRequestContextWithCustomSpan } from "effect-app/setupRequest"
|
|
6
|
-
import { pretty } from "effect-app/utils"
|
|
7
|
-
import * as Cause from "effect/Cause"
|
|
8
|
-
import * as Fiber from "effect/Fiber"
|
|
9
|
-
import { flow } from "effect/Function"
|
|
10
|
-
import * as Q from "effect/Queue"
|
|
11
|
-
import * as Tracer from "effect/Tracer"
|
|
12
|
-
import { InfraLogger } from "../logger.ts"
|
|
13
|
-
import { MemQueue } from "../memQueue.ts"
|
|
14
|
-
import { messagingSpanArgs } from "../otel.ts"
|
|
15
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause } from "./errors.ts"
|
|
16
|
-
|
|
17
|
-
export const makeMemQueue = Effect.fnUntraced(function*<
|
|
18
|
-
Evt extends { id: S.StringId; _tag: string },
|
|
19
|
-
DrainEvt extends { id: S.StringId; _tag: string },
|
|
20
|
-
EvtE,
|
|
21
|
-
DrainEvtE
|
|
22
|
-
>(
|
|
23
|
-
queueName: string,
|
|
24
|
-
queueDrainName: string,
|
|
25
|
-
schema: S.Codec<Evt, EvtE>,
|
|
26
|
-
drainSchema: S.Codec<DrainEvt, DrainEvtE>
|
|
27
|
-
) {
|
|
28
|
-
const mem = yield* MemQueue
|
|
29
|
-
const q = yield* mem.getOrCreateQueue(queueName)
|
|
30
|
-
const qDrain = yield* mem.getOrCreateQueue(queueDrainName)
|
|
31
|
-
|
|
32
|
-
const wireSchema = S.Struct({ body: schema, meta: QueueMeta })
|
|
33
|
-
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
34
|
-
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
35
|
-
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
36
|
-
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
37
|
-
|
|
38
|
-
const parseDrain = flow(S.decodeUnknownEffectConcurrently(drainWJson), Effect.orDie)
|
|
39
|
-
|
|
40
|
-
const queue = {
|
|
41
|
-
publish: Effect.fn(`publish ${queueName}`, {
|
|
42
|
-
kind: "producer",
|
|
43
|
-
attributes: {
|
|
44
|
-
"messaging.system": "memory",
|
|
45
|
-
"messaging.operation.name": "publish",
|
|
46
|
-
"messaging.destination.name": queueName
|
|
47
|
-
}
|
|
48
|
-
})(function*(
|
|
49
|
-
...messages: NonEmptyReadonlyArray<Evt>
|
|
50
|
-
) {
|
|
51
|
-
yield* Effect.annotateCurrentSpan({
|
|
52
|
-
"messaging.batch.message_count": messages.length,
|
|
53
|
-
"messaging.message.types": messages.map((_) => _._tag)
|
|
54
|
-
})
|
|
55
|
-
const requestContext = yield* getRequestContext
|
|
56
|
-
// we JSON encode, because that is what the wire also does, and it reveals holes in e.g unknown encoders (Date->String)
|
|
57
|
-
yield* Effect.forEach(
|
|
58
|
-
messages,
|
|
59
|
-
(m) =>
|
|
60
|
-
encodePublish({ body: m, meta: requestContext }).pipe(
|
|
61
|
-
Effect.orDie,
|
|
62
|
-
Effect.flatMap((_) => Q.offer(q, _))
|
|
63
|
-
),
|
|
64
|
-
{ discard: true }
|
|
65
|
-
)
|
|
66
|
-
}),
|
|
67
|
-
drain: <DrainE, DrainR>(
|
|
68
|
-
handleEvent: (ks: DrainEvt) => Effect.Effect<void, DrainE, DrainR>,
|
|
69
|
-
sessionId?: string
|
|
70
|
-
) => {
|
|
71
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name: "MemQueue.drain." + queueDrainName })
|
|
72
|
-
const reportError = reportNonInterruptedFailureCause({ name: "MemQueue.drain." + queueDrainName })
|
|
73
|
-
const processMessage = Effect.fnUntraced(function*(msg: string) {
|
|
74
|
-
// we JSON parse, because that is what the wire also does, and it reveals holes in e.g unknown encoders (Date->String)
|
|
75
|
-
const { body, meta } = yield* parseDrain(msg).pipe(Effect.orDie)
|
|
76
|
-
let effect = InfraLogger
|
|
77
|
-
.logDebug(`[${queueDrainName}] Processing incoming message`)
|
|
78
|
-
.pipe(
|
|
79
|
-
Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
|
|
80
|
-
Effect.andThen(handleEvent(body)),
|
|
81
|
-
silenceAndReportError,
|
|
82
|
-
(_) => {
|
|
83
|
-
const args = messagingSpanArgs({
|
|
84
|
-
operation: "process",
|
|
85
|
-
system: "memory",
|
|
86
|
-
destination: queueDrainName,
|
|
87
|
-
messageId: body.id,
|
|
88
|
-
conversationId: sessionId,
|
|
89
|
-
extra: { "messaging.message.type": body._tag, "messaging.message.body": body }
|
|
90
|
-
}, "consumer")
|
|
91
|
-
return setupRequestContextWithCustomSpan(
|
|
92
|
-
_,
|
|
93
|
-
meta,
|
|
94
|
-
args.name,
|
|
95
|
-
{
|
|
96
|
-
captureStackTrace: false,
|
|
97
|
-
kind: args.kind,
|
|
98
|
-
attributes: args.attributes
|
|
99
|
-
}
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
if (meta.span) {
|
|
104
|
-
effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
|
|
105
|
-
}
|
|
106
|
-
return yield* effect
|
|
107
|
-
})
|
|
108
|
-
return Effect.fn(`receive ${queueDrainName}`, {
|
|
109
|
-
kind: "consumer",
|
|
110
|
-
attributes: {
|
|
111
|
-
"messaging.system": "memory",
|
|
112
|
-
"messaging.operation.name": "receive",
|
|
113
|
-
"messaging.destination.name": queueDrainName,
|
|
114
|
-
...(sessionId !== undefined && { "messaging.message.conversation_id": sessionId })
|
|
115
|
-
}
|
|
116
|
-
})(function*() {
|
|
117
|
-
const x = yield* Q.take(qDrain)
|
|
118
|
-
const exit = yield* processMessage(x).pipe(
|
|
119
|
-
Effect.uninterruptible,
|
|
120
|
-
Effect.forkChild,
|
|
121
|
-
Effect.flatMap(Fiber.join)
|
|
122
|
-
)
|
|
123
|
-
if (exit._tag === "Failure" && !Cause.hasInterruptsOnly(exit.cause)) {
|
|
124
|
-
// normally a failed item would be returned to the queue and retried up to X times.
|
|
125
|
-
yield* Q.offer(qDrain, x).pipe(
|
|
126
|
-
// TODO: retry count tracking and max retries.
|
|
127
|
-
Effect.delay("5 seconds"),
|
|
128
|
-
Effect.tapCause(reportError),
|
|
129
|
-
Effect.forkDetach
|
|
130
|
-
)
|
|
131
|
-
}
|
|
132
|
-
}, (effect) => effect.pipe(silenceAndReportError, Effect.forever))()
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
return queue
|
|
136
|
-
})
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import type { NonEmptyReadonlyArray } from "effect-app/Array"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import { QueueMeta } from "effect-app/QueueMaker"
|
|
4
|
-
import * as S from "effect-app/Schema"
|
|
5
|
-
import type { StringId } from "effect-app/Schema"
|
|
6
|
-
import { getRequestContext, setupRequestContextWithCustomSpan } from "effect-app/setupRequest"
|
|
7
|
-
import { pretty } from "effect-app/utils"
|
|
8
|
-
import * as Cause from "effect/Cause"
|
|
9
|
-
import { flow } from "effect/Function"
|
|
10
|
-
import * as Tracer from "effect/Tracer"
|
|
11
|
-
import { InfraLogger } from "../logger.ts"
|
|
12
|
-
import { messagingSpanArgs } from "../otel.ts"
|
|
13
|
-
import { Receiver, Sender } from "../ServiceBus.ts"
|
|
14
|
-
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.ts"
|
|
15
|
-
|
|
16
|
-
export function makeServiceBusQueue<
|
|
17
|
-
Evt extends { id: StringId; _tag: string },
|
|
18
|
-
DrainEvt extends { id: StringId; _tag: string },
|
|
19
|
-
EvtE,
|
|
20
|
-
DrainEvtE
|
|
21
|
-
>(
|
|
22
|
-
schema: S.Codec<Evt, EvtE>,
|
|
23
|
-
drainSchema: S.Codec<DrainEvt, DrainEvtE>
|
|
24
|
-
) {
|
|
25
|
-
const wireSchema = S.Struct({
|
|
26
|
-
body: schema,
|
|
27
|
-
meta: QueueMeta
|
|
28
|
-
})
|
|
29
|
-
const wireSchemaJson = S.fromJsonString(S.toCodecJson(wireSchema))
|
|
30
|
-
const encodePublish = S.encodeEffect(wireSchemaJson)
|
|
31
|
-
const drainW = S.Struct({ body: drainSchema, meta: QueueMeta })
|
|
32
|
-
const drainWJson = S.fromJsonString(S.toCodecJson(drainW))
|
|
33
|
-
const parseDrain = flow(S.decodeUnknownEffectConcurrently(drainWJson), Effect.orDie)
|
|
34
|
-
|
|
35
|
-
return Effect.gen(function*() {
|
|
36
|
-
const sender = yield* Sender
|
|
37
|
-
const receiver = yield* Receiver
|
|
38
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name: receiver.name })
|
|
39
|
-
const reportError = reportNonInterruptedFailureCause({ name: receiver.name })
|
|
40
|
-
|
|
41
|
-
// TODO: or do async?
|
|
42
|
-
// This will make sure that the host receives the error (MainFiberSet.join), who will then interrupt everything and commence a shutdown and restart of app
|
|
43
|
-
// const deferred = yield* Deferred.make<never, ServiceBusError | Error>()
|
|
44
|
-
|
|
45
|
-
const queue = {
|
|
46
|
-
drain: <DrainE, DrainR>(
|
|
47
|
-
handleEvent: (ks: DrainEvt) => Effect.Effect<void, DrainE, DrainR>,
|
|
48
|
-
sessionId?: string
|
|
49
|
-
) => {
|
|
50
|
-
const processMessage = Effect.fnUntraced(function*(messageBody: unknown) {
|
|
51
|
-
const { body, meta } = yield* parseDrain(messageBody).pipe(Effect.orDie)
|
|
52
|
-
let effect = InfraLogger
|
|
53
|
-
.logDebug(`[${receiver.name}] Processing incoming message`)
|
|
54
|
-
.pipe(
|
|
55
|
-
Effect.annotateLogs({ body: pretty(body), meta: pretty(meta) }),
|
|
56
|
-
Effect.andThen(handleEvent(body)),
|
|
57
|
-
Effect.orDie,
|
|
58
|
-
// we silenceAndReportError here, so that the error is reported, and moves into the Exit.
|
|
59
|
-
silenceAndReportError,
|
|
60
|
-
(_) => {
|
|
61
|
-
const args = messagingSpanArgs({
|
|
62
|
-
operation: "process",
|
|
63
|
-
system: "servicebus",
|
|
64
|
-
destination: receiver.name,
|
|
65
|
-
messageId: body.id,
|
|
66
|
-
conversationId: sessionId,
|
|
67
|
-
extra: { "messaging.message.type": body._tag, "messaging.message.body": body }
|
|
68
|
-
}, "consumer")
|
|
69
|
-
return setupRequestContextWithCustomSpan(
|
|
70
|
-
_,
|
|
71
|
-
meta,
|
|
72
|
-
args.name,
|
|
73
|
-
{
|
|
74
|
-
captureStackTrace: false,
|
|
75
|
-
kind: args.kind,
|
|
76
|
-
attributes: args.attributes
|
|
77
|
-
}
|
|
78
|
-
)
|
|
79
|
-
}
|
|
80
|
-
)
|
|
81
|
-
if (meta.span) {
|
|
82
|
-
effect = Effect.withParentSpan(effect, Tracer.externalSpan(meta.span))
|
|
83
|
-
}
|
|
84
|
-
// we reportError here, so that we report the error only, and keep flowing
|
|
85
|
-
const exit = yield* Effect.tapCause(effect, reportError)
|
|
86
|
-
return yield* exit
|
|
87
|
-
})
|
|
88
|
-
|
|
89
|
-
return receiver
|
|
90
|
-
.subscribe({
|
|
91
|
-
processMessage: (x) => processMessage(x.body).pipe(Effect.uninterruptible),
|
|
92
|
-
processError: (err) => reportQueueError(Cause.fail(err.error))
|
|
93
|
-
}, sessionId)
|
|
94
|
-
.pipe(Effect.andThen(Effect.never))
|
|
95
|
-
},
|
|
96
|
-
|
|
97
|
-
publish: Effect.fn(`publish ${sender.name}`, {
|
|
98
|
-
kind: "producer",
|
|
99
|
-
attributes: {
|
|
100
|
-
"messaging.system": "servicebus",
|
|
101
|
-
"messaging.operation.name": "publish",
|
|
102
|
-
"messaging.destination.name": sender.name
|
|
103
|
-
}
|
|
104
|
-
})(function*(...messages: NonEmptyReadonlyArray<Evt>) {
|
|
105
|
-
yield* Effect.annotateCurrentSpan({
|
|
106
|
-
"messaging.batch.message_count": messages.length,
|
|
107
|
-
"messaging.message.types": messages.map((_) => _._tag)
|
|
108
|
-
})
|
|
109
|
-
const requestContext = yield* getRequestContext
|
|
110
|
-
const msgs = yield* Effect.forEach(messages, (m) =>
|
|
111
|
-
encodePublish({ body: m, meta: requestContext }).pipe(
|
|
112
|
-
Effect.orDie,
|
|
113
|
-
Effect.map((body) => ({
|
|
114
|
-
body,
|
|
115
|
-
messageId: m.id, /* correllationid: requestId */
|
|
116
|
-
contentType: "application/json",
|
|
117
|
-
sessionId: "sessionId" in m ? m.sessionId as string : undefined as unknown as string // TODO: optional
|
|
118
|
-
}))
|
|
119
|
-
))
|
|
120
|
-
yield* sender.sendMessages(msgs)
|
|
121
|
-
})
|
|
122
|
-
}
|
|
123
|
-
return queue
|
|
124
|
-
})
|
|
125
|
-
}
|
package/src/RequestFiberSet.ts
DELETED
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
-
import * as Context from "effect-app/Context"
|
|
3
|
-
import * as Effect from "effect-app/Effect"
|
|
4
|
-
import * as Layer from "effect-app/Layer"
|
|
5
|
-
import * as Option from "effect-app/Option"
|
|
6
|
-
import * as Fiber from "effect/Fiber"
|
|
7
|
-
import * as FiberSet from "effect/FiberSet"
|
|
8
|
-
import type * as Tracer from "effect/Tracer"
|
|
9
|
-
import { InfraLogger } from "./logger.ts"
|
|
10
|
-
import { reportRequestError, reportUnknownRequestError } from "./reportError.ts"
|
|
11
|
-
|
|
12
|
-
const getRootParentSpan = Effect.gen(function*() {
|
|
13
|
-
let span: Tracer.AnySpan | null = yield* Effect.currentSpan.pipe(
|
|
14
|
-
Effect.catchTag("NoSuchElementError", () => Effect.succeed(null))
|
|
15
|
-
)
|
|
16
|
-
if (!span) return span
|
|
17
|
-
while (span._tag === "Span" && Option.isSome(span.parent)) {
|
|
18
|
-
span = span.parent.value
|
|
19
|
-
}
|
|
20
|
-
return span
|
|
21
|
-
})
|
|
22
|
-
|
|
23
|
-
export const setRootParentSpan = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
24
|
-
getRootParentSpan.pipe(Effect.andThen((span) => span ? Effect.withParentSpan(self, span) : self))
|
|
25
|
-
|
|
26
|
-
const make = Effect.gen(function*() {
|
|
27
|
-
const set = yield* FiberSet.make<any, any>()
|
|
28
|
-
const add = (...fibers: Fiber.Fiber<any, any>[]) =>
|
|
29
|
-
Effect.sync(() => fibers.forEach((_) => FiberSet.addUnsafe(set, _)))
|
|
30
|
-
const addAll = (fibers: readonly Fiber.Fiber<any, any>[]) =>
|
|
31
|
-
Effect.sync(() => fibers.forEach((_) => FiberSet.addUnsafe(set, _)))
|
|
32
|
-
const join = FiberSet.size(set).pipe(
|
|
33
|
-
Effect.andThen((count) => InfraLogger.logInfo(`Joining ${count} current fibers on the RequestFiberSet`)),
|
|
34
|
-
Effect.andThen(FiberSet.join(set))
|
|
35
|
-
)
|
|
36
|
-
const run = FiberSet.run(set)
|
|
37
|
-
const register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
38
|
-
self.pipe(Effect.forkChild, Effect.tap(add), Effect.andThen(Fiber.join))
|
|
39
|
-
|
|
40
|
-
// const waitUntilEmpty = Effect.gen(function*() {
|
|
41
|
-
// const currentSize = yield* FiberSet.size(set)
|
|
42
|
-
// if (currentSize === 0) {
|
|
43
|
-
// return
|
|
44
|
-
// }
|
|
45
|
-
// yield* Effect.logInfo("Waiting RequestFiberSet to be empty: " + currentSize)
|
|
46
|
-
// while ((yield* FiberSet.size(set)) > 0) yield* Effect.sleep("250 millis")
|
|
47
|
-
// yield* Effect.logDebug("RequestFiberSet is empty")
|
|
48
|
-
// })
|
|
49
|
-
// TODO: loop and interrupt all fibers in the set continuously?
|
|
50
|
-
const interrupt = Fiber.interruptAll(set)
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
|
|
54
|
-
* new fiber isn't attached to the parent, when the fiber executing the
|
|
55
|
-
* returned effect terminates, the forked fiber will continue running.
|
|
56
|
-
* The fiber will be interrupted when the RequestFiberSet scope is closed.
|
|
57
|
-
*
|
|
58
|
-
* Reports errors.
|
|
59
|
-
*/
|
|
60
|
-
function forkDaemonReport<R, E, A>(self: Effect.Effect<A, E, R>) {
|
|
61
|
-
return self.pipe(
|
|
62
|
-
reportRequestError,
|
|
63
|
-
Effect.uninterruptible,
|
|
64
|
-
run
|
|
65
|
-
)
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/**
|
|
69
|
-
* Forks the effect into a new fiber attached to the RequestFiberSet scope. Because the
|
|
70
|
-
* new fiber isn't attached to the parent, when the fiber executing the
|
|
71
|
-
* returned effect terminates, the forked fiber will continue running.
|
|
72
|
-
* The fiber will be interrupted when the RequestFiberSet scope is closed.
|
|
73
|
-
*
|
|
74
|
-
* Reports unexpected errors.
|
|
75
|
-
*/
|
|
76
|
-
function forkDaemonReportUnexpected<R, E, A>(self: Effect.Effect<A, E, R>) {
|
|
77
|
-
return self
|
|
78
|
-
.pipe(
|
|
79
|
-
reportUnknownRequestError,
|
|
80
|
-
Effect.uninterruptible,
|
|
81
|
-
run
|
|
82
|
-
)
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
interrupt,
|
|
87
|
-
join,
|
|
88
|
-
run,
|
|
89
|
-
add,
|
|
90
|
-
addAll,
|
|
91
|
-
register,
|
|
92
|
-
forkDaemonReport,
|
|
93
|
-
forkDaemonReportUnexpected
|
|
94
|
-
}
|
|
95
|
-
})
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Whenever you fork a fiber for a Request, and you want to prevent dependent services to close prematurely on interruption,
|
|
99
|
-
* like the ServiceBus Sender, you should register these fibers in this FiberSet.
|
|
100
|
-
*/
|
|
101
|
-
export class RequestFiberSet extends Context.Service<RequestFiberSet>()("RequestFiberSet", { make }) {
|
|
102
|
-
static readonly Live = Layer.effect(this, this.make)
|
|
103
|
-
static readonly register = <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
104
|
-
this.pipe(Effect.andThen((_) => _.register(self)))
|
|
105
|
-
static readonly run = <A, E, R>(self: Effect.Effect<A, E, R>) => this.pipe(Effect.andThen((_) => _.run(self)))
|
|
106
|
-
static readonly forkDaemonReport = <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
107
|
-
this.pipe(Effect.andThen((_) => _.forkDaemonReport(self)))
|
|
108
|
-
static readonly forkDaemonReportUnexpected = <R, E, A>(self: Effect.Effect<A, E, R>) =>
|
|
109
|
-
this.pipe(Effect.andThen((_) => _.forkDaemonReportUnexpected(self)))
|
|
110
|
-
}
|
package/src/ServiceBus.ts
DELETED
|
@@ -1,219 +0,0 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
2
|
-
import { type OperationOptionsBase, type ProcessErrorArgs, ServiceBusClient, type ServiceBusMessage, type ServiceBusMessageBatch, type ServiceBusReceivedMessage, type ServiceBusReceiver } from "@azure/service-bus"
|
|
3
|
-
import * as Context from "effect-app/Context"
|
|
4
|
-
import * as Effect from "effect-app/Effect"
|
|
5
|
-
import * as Layer from "effect-app/Layer"
|
|
6
|
-
import * as Cause from "effect/Cause"
|
|
7
|
-
import * as Exit from "effect/Exit"
|
|
8
|
-
import * as FiberSet from "effect/FiberSet"
|
|
9
|
-
import type * as Scope from "effect/Scope"
|
|
10
|
-
import { InfraLogger } from "./logger.ts"
|
|
11
|
-
|
|
12
|
-
const logged = (name: string) => <A, E, R>(self: Effect.Effect<A, E, R>) =>
|
|
13
|
-
Effect.logInfo(name).pipe(
|
|
14
|
-
Effect.andThen(self),
|
|
15
|
-
Effect.tap(Effect.logInfo(name + " done")),
|
|
16
|
-
Effect.withLogSpan(name)
|
|
17
|
-
)
|
|
18
|
-
|
|
19
|
-
function makeClient(url: string) {
|
|
20
|
-
return Effect.acquireRelease(
|
|
21
|
-
Effect.sync(() => new ServiceBusClient(url)).pipe(logged("ServiceBus.client.create")),
|
|
22
|
-
(client) => Effect.promise(() => client.close()).pipe(logged("ServiceBus.client.close"))
|
|
23
|
-
)
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export class ServiceBusClientTag
|
|
27
|
-
extends Context.Opaque<ServiceBusClientTag, ServiceBusClient>()("@services/Client", { make: makeClient })
|
|
28
|
-
{
|
|
29
|
-
static readonly layer = (url: string) => this.toLayer(this.make(url))
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
const makeSender_ = Effect.fnUntraced(function*(queueName: string) {
|
|
33
|
-
const serviceBusClient = yield* ServiceBusClientTag
|
|
34
|
-
|
|
35
|
-
return yield* Effect.acquireRelease(
|
|
36
|
-
Effect.sync(() => serviceBusClient.createSender(queueName)).pipe(
|
|
37
|
-
logged(`ServiceBus.sender.create ${queueName}`)
|
|
38
|
-
),
|
|
39
|
-
(sender) => Effect.promise(() => sender.close()).pipe(logged(`ServiceBus.sender.close ${queueName}`))
|
|
40
|
-
)
|
|
41
|
-
})
|
|
42
|
-
|
|
43
|
-
const makeSender = Effect.fnUntraced(function*(name: string) {
|
|
44
|
-
const sender = yield* makeSender_(name)
|
|
45
|
-
const sendMessages = Effect.fnUntraced(function*(
|
|
46
|
-
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
47
|
-
options?: Omit<OperationOptionsBase, "abortSignal">
|
|
48
|
-
) {
|
|
49
|
-
return yield* Effect.promise((abortSignal) => sender.sendMessages(messages, { ...options, abortSignal }))
|
|
50
|
-
})
|
|
51
|
-
|
|
52
|
-
return { name, sendMessages }
|
|
53
|
-
})
|
|
54
|
-
|
|
55
|
-
export class Sender extends Context.Opaque<Sender, {
|
|
56
|
-
name: string
|
|
57
|
-
sendMessages: (
|
|
58
|
-
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
59
|
-
options?: Omit<OperationOptionsBase, "abortSignal">
|
|
60
|
-
) => Effect.Effect<void>
|
|
61
|
-
}>()("Sender", { make: makeSender }) {
|
|
62
|
-
static readonly layer = (name: string) => this.toLayer(this.make(name))
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export const SenderTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
66
|
-
const tag = Context.Service<Id, Sender>(`ServiceBus.Sender.${queueName}`)
|
|
67
|
-
|
|
68
|
-
return Object.assign(tag, {
|
|
69
|
-
layer: Layer.effect(
|
|
70
|
-
tag,
|
|
71
|
-
Sender.make(queueName).pipe(Effect.map(Sender.of))
|
|
72
|
-
)
|
|
73
|
-
})
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const makeReceiver = Effect.fnUntraced(function*(name: string) {
|
|
77
|
-
const serviceBusClient = yield* ServiceBusClientTag
|
|
78
|
-
|
|
79
|
-
const makeReceiver = Effect.fnUntraced(
|
|
80
|
-
function*(queueName: string, waitTillEmpty: Effect.Effect<void>, sessionId?: string) {
|
|
81
|
-
const annotate = sessionId !== undefined
|
|
82
|
-
? Effect.annotateLogs({ "messaging.session.id": sessionId })
|
|
83
|
-
: <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => self
|
|
84
|
-
return yield* Effect.acquireRelease(
|
|
85
|
-
(sessionId
|
|
86
|
-
? Effect.promise(() => serviceBusClient.acceptSession(queueName, sessionId))
|
|
87
|
-
: Effect.sync(() => serviceBusClient.createReceiver(queueName)))
|
|
88
|
-
.pipe(logged(`ServiceBus.receiver.create ${queueName}`), annotate),
|
|
89
|
-
(r) =>
|
|
90
|
-
waitTillEmpty.pipe(
|
|
91
|
-
logged(`ServiceBus.receiver.waitTillEmpty ${queueName}`),
|
|
92
|
-
Effect.andThen(
|
|
93
|
-
Effect.promise(() => r.close()).pipe(
|
|
94
|
-
logged(`ServiceBus.receiver.close ${queueName}`)
|
|
95
|
-
)
|
|
96
|
-
),
|
|
97
|
-
logged(`ServiceBus.receiver.release ${queueName}`),
|
|
98
|
-
annotate
|
|
99
|
-
)
|
|
100
|
-
)
|
|
101
|
-
}
|
|
102
|
-
)
|
|
103
|
-
|
|
104
|
-
const make = (waitTillEmpty: Effect.Effect<void>) => makeReceiver(name, waitTillEmpty)
|
|
105
|
-
|
|
106
|
-
const makeSession = (sessionId: string, waitTillEmpty: Effect.Effect<void>) =>
|
|
107
|
-
makeReceiver(name, waitTillEmpty, sessionId)
|
|
108
|
-
|
|
109
|
-
return {
|
|
110
|
-
name,
|
|
111
|
-
make,
|
|
112
|
-
makeSession,
|
|
113
|
-
subscribe: Effect.fnUntraced(function*<RMsg, RErr>(hndlr: MessageHandlers<RMsg, RErr>, sessionId?: string) {
|
|
114
|
-
const fs = yield* FiberSet.make()
|
|
115
|
-
const fr = yield* FiberSet.runtime(fs)<RMsg | RErr>()
|
|
116
|
-
const wait = Effect
|
|
117
|
-
.gen(function*() {
|
|
118
|
-
if ((yield* FiberSet.size(fs)) > 0) {
|
|
119
|
-
yield* InfraLogger.logDebug("Waiting ServiceBusFiberSet to be empty: " + (yield* FiberSet.size(fs)))
|
|
120
|
-
}
|
|
121
|
-
while ((yield* FiberSet.size(fs)) > 0) yield* Effect.sleep("250 millis")
|
|
122
|
-
})
|
|
123
|
-
const r = yield* sessionId
|
|
124
|
-
? makeSession(
|
|
125
|
-
sessionId,
|
|
126
|
-
wait
|
|
127
|
-
)
|
|
128
|
-
: make(wait)
|
|
129
|
-
|
|
130
|
-
const runEffect = <E>(effect: Effect.Effect<void, E, RMsg | RErr>) =>
|
|
131
|
-
new Promise<void>((resolve, reject) =>
|
|
132
|
-
fr(effect)
|
|
133
|
-
.addObserver((exit) => {
|
|
134
|
-
if (Exit.isSuccess(exit)) {
|
|
135
|
-
resolve(exit.value)
|
|
136
|
-
} else {
|
|
137
|
-
// disable @typescript-eslint/prefer-promise-reject-errors
|
|
138
|
-
reject(Cause.pretty(exit.cause))
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
)
|
|
142
|
-
|
|
143
|
-
const annotate = sessionId !== undefined
|
|
144
|
-
? Effect.annotateLogs({ "messaging.session.id": sessionId })
|
|
145
|
-
: <A, E, R>(self: Effect.Effect<A, E, R>): Effect.Effect<A, E, R> => self
|
|
146
|
-
yield* Effect.acquireRelease(
|
|
147
|
-
Effect
|
|
148
|
-
.sync(() => {
|
|
149
|
-
const s = r
|
|
150
|
-
.subscribe({
|
|
151
|
-
processError: (err) =>
|
|
152
|
-
runEffect(
|
|
153
|
-
hndlr
|
|
154
|
-
.processError(err)
|
|
155
|
-
.pipe(
|
|
156
|
-
Effect.catchCause((cause) => Effect.logError("ServiceBus Error", cause)),
|
|
157
|
-
annotate
|
|
158
|
-
)
|
|
159
|
-
),
|
|
160
|
-
processMessage: (msg) => runEffect(hndlr.processMessage(msg).pipe(annotate))
|
|
161
|
-
// DO NOT CATCH ERRORS here as they should return to the queue!
|
|
162
|
-
})
|
|
163
|
-
return { close: Effect.promise(() => s.close()) }
|
|
164
|
-
})
|
|
165
|
-
.pipe(logged("ServiceBus.subscription.create"), annotate),
|
|
166
|
-
(subscription) =>
|
|
167
|
-
subscription.close.pipe(
|
|
168
|
-
logged("ServiceBus.subscription.close"),
|
|
169
|
-
annotate
|
|
170
|
-
)
|
|
171
|
-
) as Effect.Effect<void, never, Scope.Scope> // wth is going on here
|
|
172
|
-
})
|
|
173
|
-
}
|
|
174
|
-
})
|
|
175
|
-
|
|
176
|
-
export class Receiver extends Context.Opaque<Receiver, {
|
|
177
|
-
name: string
|
|
178
|
-
make: (waitTillEmpty: Effect.Effect<void>) => Effect.Effect<ServiceBusReceiver, never, Scope.Scope>
|
|
179
|
-
makeSession: (
|
|
180
|
-
sessionId: string,
|
|
181
|
-
waitTillEmpty: Effect.Effect<void>
|
|
182
|
-
) => Effect.Effect<ServiceBusReceiver, never, Scope.Scope>
|
|
183
|
-
subscribe<RMsg, RErr>(
|
|
184
|
-
hndlr: MessageHandlers<RMsg, RErr>,
|
|
185
|
-
sessionId?: string
|
|
186
|
-
): Effect.Effect<void, never, Scope.Scope | RMsg | RErr>
|
|
187
|
-
}>()("Receiver") {
|
|
188
|
-
static readonly make = makeReceiver
|
|
189
|
-
static readonly layer = (name: string) => this.toLayer(makeReceiver(name))
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
export const ReceiverTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
193
|
-
const tag = Context.Service<Id, Receiver>(`ServiceBus.Receiver.${queueName}`)
|
|
194
|
-
|
|
195
|
-
return Object.assign(tag, {
|
|
196
|
-
layer: Layer.effect(
|
|
197
|
-
tag,
|
|
198
|
-
makeReceiver(queueName).pipe(Effect.map(Receiver.of))
|
|
199
|
-
)
|
|
200
|
-
})
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
export const SenderReceiver = (queue: string, queueDrain?: string) =>
|
|
204
|
-
Layer.mergeAll(Sender.layer(queue), Receiver.layer(queueDrain ?? queue))
|
|
205
|
-
|
|
206
|
-
export interface MessageHandlers<RMsg, RErr> {
|
|
207
|
-
/**
|
|
208
|
-
* Handler that processes messages from service bus.
|
|
209
|
-
*
|
|
210
|
-
* @param message - A message received from Service Bus.
|
|
211
|
-
*/
|
|
212
|
-
processMessage(message: ServiceBusReceivedMessage): Effect.Effect<void, never, RMsg>
|
|
213
|
-
/**
|
|
214
|
-
* Handler that processes errors that occur during receiving.
|
|
215
|
-
* @param args - The error and additional context to indicate where
|
|
216
|
-
* the error originated.
|
|
217
|
-
*/
|
|
218
|
-
processError(args: ProcessErrorArgs): Effect.Effect<void, never, RErr>
|
|
219
|
-
}
|
package/src/memQueue.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import * as Context from "effect-app/Context"
|
|
2
|
-
import * as Effect from "effect-app/Effect"
|
|
3
|
-
import * as Queue from "effect/Queue"
|
|
4
|
-
|
|
5
|
-
const make = Effect
|
|
6
|
-
.gen(function*() {
|
|
7
|
-
const store = yield* Effect.sync(() => new Map<string, Queue.Queue<string>>())
|
|
8
|
-
|
|
9
|
-
return {
|
|
10
|
-
getOrCreateQueue: Effect.fnUntraced(function*(k: string) {
|
|
11
|
-
const q = store.get(k)
|
|
12
|
-
if (q) return q
|
|
13
|
-
const newQ = yield* Queue.unbounded<string>()
|
|
14
|
-
store.set(k, newQ)
|
|
15
|
-
return newQ
|
|
16
|
-
})
|
|
17
|
-
}
|
|
18
|
-
})
|
|
19
|
-
|
|
20
|
-
export class MemQueue extends Context.Opaque<MemQueue>()("effect-app/MemQueue", { make }) {
|
|
21
|
-
static readonly Live = this.toLayer(this.make)
|
|
22
|
-
}
|