@effect-app/infra 2.92.2 → 2.93.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/CHANGELOG.md +17 -0
- package/dist/MainFiberSet.d.ts +1 -1
- package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
- package/dist/Model/Repository/internal/internal.js +38 -37
- package/dist/Operations.d.ts +1 -1
- package/dist/QueueMaker/sbqueue.d.ts +5 -6
- package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
- package/dist/QueueMaker/sbqueue.js +18 -22
- package/dist/RequestFiberSet.d.ts +1 -1
- package/dist/Store/service.d.ts +1 -1
- package/dist/adapters/ServiceBus.d.ts +57 -17
- package/dist/adapters/ServiceBus.d.ts.map +1 -1
- package/dist/adapters/ServiceBus.js +75 -61
- package/dist/adapters/memQueue.d.ts +1 -1
- package/dist/api/routing/middleware/middleware-api.d.ts +2 -1
- package/dist/api/routing/middleware/middleware-api.d.ts.map +1 -1
- package/dist/api/routing/middleware/middleware-api.js +1 -1
- package/dist/api/routing/middleware.d.ts +0 -1
- package/dist/api/routing/middleware.d.ts.map +1 -1
- package/dist/api/routing/middleware.js +1 -2
- package/dist/api/routing.d.ts +8 -9
- package/dist/api/routing.d.ts.map +1 -1
- package/dist/api/routing.js +18 -10
- package/package.json +2 -6
- package/src/Model/Repository/internal/internal.ts +45 -41
- package/src/QueueMaker/sbqueue.ts +33 -46
- package/src/adapters/ServiceBus.ts +141 -93
- package/src/api/routing/middleware/middleware-api.ts +2 -1
- package/src/api/routing/middleware.ts +0 -1
- package/src/api/routing.ts +46 -233
- package/test/controller.test.ts +12 -8
- package/test/dist/controller.test.d.ts.map +1 -1
- package/test/dist/fixtures.d.ts +3 -3
- package/dist/api/routing/middleware/dynamic-middleware.d.ts +0 -2
- package/dist/api/routing/middleware/dynamic-middleware.d.ts.map +0 -1
- package/dist/api/routing/middleware/dynamic-middleware.js +0 -2
- package/src/api/routing/middleware/dynamic-middleware.ts +0 -0
|
@@ -126,8 +126,10 @@ export function makeRepoInternal<
|
|
|
126
126
|
: s.pipe(S.pick(idKey as any))
|
|
127
127
|
})
|
|
128
128
|
const encodeId = flow(S.encode(i), provideRctx)
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
const findEId = Effect.fnUntraced(function*(id: Encoded[IdKey]) {
|
|
130
|
+
yield* Effect.annotateCurrentSpan({ itemId: id })
|
|
131
|
+
|
|
132
|
+
return yield* Effect.flatMap(
|
|
131
133
|
store.find(id),
|
|
132
134
|
(item) =>
|
|
133
135
|
Effect.gen(function*() {
|
|
@@ -135,20 +137,24 @@ export function makeRepoInternal<
|
|
|
135
137
|
return item.pipe(Option.map((_) => mapReverse(_, set)))
|
|
136
138
|
})
|
|
137
139
|
)
|
|
138
|
-
}
|
|
140
|
+
})
|
|
139
141
|
// TODO: select the particular field, instead of as struct
|
|
140
|
-
|
|
141
|
-
|
|
142
|
+
const findE = Effect.fnUntraced(function*(id: T[IdKey]) {
|
|
143
|
+
yield* Effect.annotateCurrentSpan({ itemId: id })
|
|
144
|
+
|
|
145
|
+
return yield* pipe(
|
|
142
146
|
encodeId({ [idKey]: id } as any),
|
|
143
147
|
Effect.orDie,
|
|
144
148
|
Effect.map((_) => (_ as any)[idKey]),
|
|
145
149
|
Effect.flatMap(findEId)
|
|
146
150
|
)
|
|
147
|
-
}
|
|
151
|
+
})
|
|
148
152
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
153
|
+
const find = Effect.fn("find")(function*(id: T[IdKey]) {
|
|
154
|
+
yield* Effect.annotateCurrentSpan({ itemId: id })
|
|
155
|
+
|
|
156
|
+
return yield* Effect.flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
|
|
157
|
+
})
|
|
152
158
|
|
|
153
159
|
const saveAllE = (a: Iterable<Encoded>) =>
|
|
154
160
|
Effect
|
|
@@ -172,40 +178,38 @@ export function makeRepoInternal<
|
|
|
172
178
|
Effect.andThen(saveAllE)
|
|
173
179
|
)
|
|
174
180
|
|
|
175
|
-
const saveAndPublish = (items: Iterable<T>, events: Iterable<Evt> = [])
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
183
|
-
(_) => Effect.flatMapOption(_, pub),
|
|
184
|
-
Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"])),
|
|
185
|
-
Effect.asVoid
|
|
186
|
-
)
|
|
187
|
-
})
|
|
188
|
-
.pipe(Effect.withSpan("saveAndPublish", { captureStackTrace: false }))
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function removeAndPublish(a: Iterable<T>, events: Iterable<Evt> = []) {
|
|
192
|
-
return Effect.gen(function*() {
|
|
193
|
-
const { get, set } = yield* cms
|
|
194
|
-
const it = [...a]
|
|
195
|
-
const items = yield* encodeMany(it).pipe(Effect.orDie)
|
|
196
|
-
// TODO: we should have a batchRemove on store so the adapter can actually batch...
|
|
197
|
-
for (const e of items) {
|
|
198
|
-
yield* store.remove(mapToPersistenceModel(e, get))
|
|
199
|
-
set(e[idKey], undefined)
|
|
200
|
-
}
|
|
201
|
-
yield* Effect
|
|
202
|
-
.sync(() => toNonEmptyArray([...events]))
|
|
181
|
+
const saveAndPublish = Effect.fn("saveAndPublish")(function*(items: Iterable<T>, events: Iterable<Evt> = []) {
|
|
182
|
+
const it = Chunk.fromIterable(items)
|
|
183
|
+
const evts = [...events]
|
|
184
|
+
yield* Effect.annotateCurrentSpan({ itemIds: [...Chunk.map(it, (_) => _[idKey])], events: evts.length })
|
|
185
|
+
return yield* saveAll(it)
|
|
186
|
+
.pipe(
|
|
187
|
+
Effect.andThen(Effect.sync(() => toNonEmptyArray(evts))),
|
|
203
188
|
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
204
|
-
|
|
189
|
+
(_) => Effect.flatMapOption(_, pub),
|
|
190
|
+
Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"])),
|
|
191
|
+
Effect.asVoid
|
|
192
|
+
)
|
|
193
|
+
})
|
|
205
194
|
|
|
206
|
-
|
|
207
|
-
}
|
|
208
|
-
|
|
195
|
+
const removeAndPublish = Effect.fn("removeAndPublish")(function*(a: Iterable<T>, events: Iterable<Evt> = []) {
|
|
196
|
+
const { get, set } = yield* cms
|
|
197
|
+
const it = [...a]
|
|
198
|
+
const evts = [...events]
|
|
199
|
+
yield* Effect.annotateCurrentSpan({ itemIds: it.map((_) => _[idKey]), eventCount: evts.length })
|
|
200
|
+
const items = yield* encodeMany(it).pipe(Effect.orDie)
|
|
201
|
+
// TODO: we should have a batchRemove on store so the adapter can actually batch...
|
|
202
|
+
for (const e of items) {
|
|
203
|
+
yield* store.remove(mapToPersistenceModel(e, get))
|
|
204
|
+
set(e[idKey], undefined)
|
|
205
|
+
}
|
|
206
|
+
yield* Effect
|
|
207
|
+
.sync(() => toNonEmptyArray(evts))
|
|
208
|
+
// TODO: for full consistency the events should be stored within the same database transaction, and then picked up.
|
|
209
|
+
.pipe((_) => Effect.flatMapOption(_, pub))
|
|
210
|
+
|
|
211
|
+
yield* changeFeed.publish([it, "remove"])
|
|
212
|
+
})
|
|
209
213
|
|
|
210
214
|
const parseMany = (items: readonly PM[]) =>
|
|
211
215
|
Effect
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
import type {} from "@azure/service-bus"
|
|
2
1
|
import { Tracer } from "effect"
|
|
3
|
-
import { Cause, Effect, flow,
|
|
2
|
+
import { Cause, Effect, flow, S } from "effect-app"
|
|
4
3
|
import type { StringId } from "effect-app/Schema"
|
|
5
4
|
import { pretty } from "effect-app/utils"
|
|
6
|
-
import {
|
|
5
|
+
import { Receiver, Sender } from "../adapters/ServiceBus.js"
|
|
7
6
|
import { getRequestContext, setupRequestContextWithCustomSpan } from "../api/setupRequest.js"
|
|
8
7
|
import { InfraLogger } from "../logger.js"
|
|
9
8
|
import { reportNonInterruptedFailure, reportNonInterruptedFailureCause, reportQueueError } from "./errors.js"
|
|
@@ -15,8 +14,6 @@ export function makeServiceBusQueue<
|
|
|
15
14
|
EvtE,
|
|
16
15
|
DrainEvtE
|
|
17
16
|
>(
|
|
18
|
-
queueName: string,
|
|
19
|
-
queueDrainName: string,
|
|
20
17
|
schema: S.Schema<Evt, EvtE>,
|
|
21
18
|
drainSchema: S.Schema<DrainEvt, DrainEvtE>
|
|
22
19
|
) {
|
|
@@ -28,10 +25,10 @@ export function makeServiceBusQueue<
|
|
|
28
25
|
const parseDrain = flow(S.decodeUnknown(drainW), Effect.orDie)
|
|
29
26
|
|
|
30
27
|
return Effect.gen(function*() {
|
|
31
|
-
const
|
|
32
|
-
const receiver = yield*
|
|
33
|
-
const silenceAndReportError = reportNonInterruptedFailure({ name:
|
|
34
|
-
const reportError = reportNonInterruptedFailureCause({ name:
|
|
28
|
+
const sender = yield* Sender
|
|
29
|
+
const receiver = yield* Receiver
|
|
30
|
+
const silenceAndReportError = reportNonInterruptedFailure({ name: receiver.name })
|
|
31
|
+
const reportError = reportNonInterruptedFailureCause({ name: receiver.name })
|
|
35
32
|
|
|
36
33
|
// TODO: or do async?
|
|
37
34
|
// 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
|
|
@@ -54,7 +51,7 @@ export function makeServiceBusQueue<
|
|
|
54
51
|
Effect
|
|
55
52
|
.flatMap(({ body, meta }) => {
|
|
56
53
|
let effect = InfraLogger
|
|
57
|
-
.logDebug(`[${
|
|
54
|
+
.logDebug(`[${receiver.name}] Processing incoming message`)
|
|
58
55
|
.pipe(
|
|
59
56
|
Effect.annotateLogs({
|
|
60
57
|
body: pretty(body),
|
|
@@ -70,12 +67,12 @@ export function makeServiceBusQueue<
|
|
|
70
67
|
setupRequestContextWithCustomSpan(
|
|
71
68
|
_,
|
|
72
69
|
meta,
|
|
73
|
-
`queue.drain: ${
|
|
70
|
+
`queue.drain: ${receiver.name}${sessionId ? `#${sessionId}` : ""}.${body._tag}`,
|
|
74
71
|
{
|
|
75
72
|
captureStackTrace: false,
|
|
76
73
|
kind: "consumer",
|
|
77
74
|
attributes: {
|
|
78
|
-
"queue.name":
|
|
75
|
+
"queue.name": receiver.name,
|
|
79
76
|
"queue.sessionId": sessionId,
|
|
80
77
|
"queue.input": body
|
|
81
78
|
}
|
|
@@ -95,16 +92,16 @@ export function makeServiceBusQueue<
|
|
|
95
92
|
)
|
|
96
93
|
}
|
|
97
94
|
|
|
98
|
-
return yield*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
95
|
+
return yield* receiver
|
|
96
|
+
.subscribe({
|
|
97
|
+
processMessage: (x) => processMessage(x.body).pipe(Effect.uninterruptible),
|
|
98
|
+
processError: (err) => reportQueueError(Cause.fail(err.error))
|
|
99
|
+
// Deferred.completeWith(
|
|
100
|
+
// deferred,
|
|
101
|
+
// reportFatalQueueError(Cause.fail(err.error))
|
|
102
|
+
// .pipe(Effect.andThen(Effect.fail(err.error)))
|
|
103
|
+
// )
|
|
104
|
+
}, sessionId)
|
|
108
105
|
})
|
|
109
106
|
// .pipe(Effect.andThen(Deferred.await(deferred).pipe(Effect.orDie))),
|
|
110
107
|
.pipe(
|
|
@@ -115,25 +112,21 @@ export function makeServiceBusQueue<
|
|
|
115
112
|
Effect
|
|
116
113
|
.gen(function*() {
|
|
117
114
|
const requestContext = yield* getRequestContext
|
|
118
|
-
return yield*
|
|
119
|
-
.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
body:
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
})),
|
|
132
|
-
{ abortSignal }
|
|
133
|
-
)
|
|
134
|
-
)
|
|
115
|
+
return yield* sender.sendMessages(
|
|
116
|
+
messages.map((m) => ({
|
|
117
|
+
body: JSON.stringify(
|
|
118
|
+
S.encodeSync(wireSchema)({
|
|
119
|
+
body: m,
|
|
120
|
+
meta: requestContext
|
|
121
|
+
})
|
|
122
|
+
),
|
|
123
|
+
messageId: m.id, /* correllationid: requestId */
|
|
124
|
+
contentType: "application/json",
|
|
125
|
+
sessionId: "sessionId" in m ? m.sessionId as string : undefined as unknown as string // TODO: optional
|
|
126
|
+
}))
|
|
127
|
+
)
|
|
135
128
|
})
|
|
136
|
-
.pipe(Effect.withSpan("queue.publish: " +
|
|
129
|
+
.pipe(Effect.withSpan("queue.publish: " + sender.name, {
|
|
137
130
|
captureStackTrace: false,
|
|
138
131
|
kind: "producer",
|
|
139
132
|
attributes: { "message_tags": messages.map((_) => _._tag) }
|
|
@@ -141,9 +134,3 @@ export function makeServiceBusQueue<
|
|
|
141
134
|
} satisfies QueueBase<Evt, DrainEvt>
|
|
142
135
|
})
|
|
143
136
|
}
|
|
144
|
-
|
|
145
|
-
export function makeServiceBusLayers(url: string, queueName: string, queueDrainName: string) {
|
|
146
|
-
return Layer.merge(ServiceBusReceiverFactory.Live(queueDrainName), LiveSender(queueName)).pipe(
|
|
147
|
-
Layer.provide(LiveServiceBusClient(url))
|
|
148
|
-
)
|
|
149
|
-
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/prefer-promise-reject-errors */
|
|
2
|
-
import { type OperationOptionsBase, type ProcessErrorArgs, ServiceBusClient, type ServiceBusMessage, type ServiceBusMessageBatch, type ServiceBusReceivedMessage, type ServiceBusReceiver
|
|
2
|
+
import { type OperationOptionsBase, type ProcessErrorArgs, ServiceBusClient, type ServiceBusMessage, type ServiceBusMessageBatch, type ServiceBusReceivedMessage, type ServiceBusReceiver } from "@azure/service-bus"
|
|
3
3
|
import { Cause, Context, Effect, Exit, FiberSet, Layer, type Scope } from "effect-app"
|
|
4
4
|
import { InfraLogger } from "../logger.js"
|
|
5
5
|
|
|
@@ -18,12 +18,13 @@ function makeClient(url: string) {
|
|
|
18
18
|
)
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
export class ServiceBusClientTag extends Context.Tag("@services/Client")<ServiceBusClientTag, ServiceBusClient>() {
|
|
22
|
+
static readonly layer = (url: string) => Layer.scoped(this, makeClient(url))
|
|
23
|
+
}
|
|
23
24
|
|
|
24
|
-
function
|
|
25
|
+
function makeSender_(queueName: string) {
|
|
25
26
|
return Effect.gen(function*() {
|
|
26
|
-
const serviceBusClient = yield*
|
|
27
|
+
const serviceBusClient = yield* ServiceBusClientTag
|
|
27
28
|
|
|
28
29
|
return yield* Effect.acquireRelease(
|
|
29
30
|
Effect.sync(() => serviceBusClient.createSender(queueName)).pipe(
|
|
@@ -33,112 +34,159 @@ function makeSender(queueName: string) {
|
|
|
33
34
|
)
|
|
34
35
|
})
|
|
35
36
|
}
|
|
36
|
-
export const Sender = Context.GenericTag<ServiceBusSender>("@services/Sender")
|
|
37
37
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
38
|
+
const makeSender = (name: string) =>
|
|
39
|
+
Effect.gen(function*() {
|
|
40
|
+
const sender = yield* makeSender_(name)
|
|
41
|
+
const sendMessages = Effect.fnUntraced(function*(
|
|
42
|
+
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
43
|
+
options?: Omit<OperationOptionsBase, "abortSignal">
|
|
44
|
+
) {
|
|
45
|
+
return yield* Effect.promise((abortSignal) => sender.sendMessages(messages, { ...options, abortSignal }))
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
return { name, sendMessages }
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
export class Sender extends Context.TagId("Sender")<Sender, {
|
|
52
|
+
name: string
|
|
53
|
+
sendMessages: (
|
|
54
|
+
messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
|
|
55
|
+
options?: Omit<OperationOptionsBase, "abortSignal"> | undefined
|
|
56
|
+
) => Effect.Effect<void, never, never>
|
|
57
|
+
}>() {
|
|
58
|
+
static readonly layer = (name: string) => this.toLayerScoped(makeSender(name))
|
|
41
59
|
}
|
|
42
60
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
61
|
+
export const SenderTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
62
|
+
const tag = Context.Tag(`ServiceBus.Sender.${queueName}`)<
|
|
63
|
+
Id,
|
|
64
|
+
Sender
|
|
65
|
+
>()
|
|
46
66
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
.pipe(withSpanAndLog(`ServiceBus.receiver.create ${queueName}.${sessionId}`)),
|
|
52
|
-
(r) =>
|
|
53
|
-
waitTillEmpty.pipe(
|
|
54
|
-
withSpanAndLog(`ServiceBus.receiver.waitTillEmpty ${queueName}.${sessionId}`),
|
|
55
|
-
Effect.andThen(
|
|
56
|
-
Effect.promise(() => r.close()).pipe(withSpanAndLog(`ServiceBus.receiver.close ${queueName}.${sessionId}`))
|
|
57
|
-
),
|
|
58
|
-
withSpanAndLog(`ServiceBus.receiver.release ${queueName}.${sessionId}`)
|
|
59
|
-
)
|
|
67
|
+
return Object.assign(tag, {
|
|
68
|
+
layer: Layer.scoped(
|
|
69
|
+
tag,
|
|
70
|
+
makeSender(queueName).pipe(Effect.map((_) => Sender.of(_)))
|
|
60
71
|
)
|
|
61
72
|
})
|
|
62
73
|
}
|
|
63
74
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
75
|
+
const makeReceiver = (name: string) =>
|
|
76
|
+
Effect.gen(function*() {
|
|
77
|
+
const serviceBusClient = yield* ServiceBusClientTag
|
|
78
|
+
|
|
79
|
+
const makeReceiver = Effect.fnUntraced(
|
|
80
|
+
function*(queueName: string, waitTillEmpty: Effect<void>, sessionId?: string) {
|
|
81
|
+
return yield* Effect.acquireRelease(
|
|
82
|
+
(sessionId
|
|
83
|
+
? Effect.promise(() => serviceBusClient.acceptSession(queueName, sessionId))
|
|
84
|
+
: Effect.sync(() => serviceBusClient.createReceiver(queueName)))
|
|
85
|
+
.pipe(withSpanAndLog(`ServiceBus.receiver.create ${queueName}.${sessionId}`)),
|
|
86
|
+
(r) =>
|
|
87
|
+
waitTillEmpty.pipe(
|
|
88
|
+
withSpanAndLog(`ServiceBus.receiver.waitTillEmpty ${queueName}.${sessionId}`),
|
|
89
|
+
Effect.andThen(
|
|
90
|
+
Effect.promise(() => r.close()).pipe(
|
|
91
|
+
withSpanAndLog(`ServiceBus.receiver.close ${queueName}.${sessionId}`)
|
|
92
|
+
)
|
|
93
|
+
),
|
|
94
|
+
withSpanAndLog(`ServiceBus.receiver.release ${queueName}.${sessionId}`)
|
|
95
|
+
)
|
|
96
|
+
)
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
const make = (waitTillEmpty: Effect<void>) => makeReceiver(name, waitTillEmpty)
|
|
101
|
+
|
|
102
|
+
const makeSession = (sessionId: string, waitTillEmpty: Effect<void>) => makeReceiver(name, waitTillEmpty, sessionId)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
name,
|
|
106
|
+
make,
|
|
107
|
+
makeSession,
|
|
108
|
+
subscribe: Effect.fnUntraced(function*<RMsg, RErr>(hndlr: MessageHandlers<RMsg, RErr>, sessionId?: string) {
|
|
109
|
+
const fs = yield* FiberSet.make()
|
|
110
|
+
const fr = yield* FiberSet.runtime(fs)<RMsg | RErr>()
|
|
111
|
+
const wait = Effect
|
|
112
|
+
.gen(function*() {
|
|
113
|
+
if ((yield* FiberSet.size(fs)) > 0) {
|
|
114
|
+
yield* InfraLogger.logDebug("Waiting ServiceBusFiberSet to be empty: " + (yield* FiberSet.size(fs)))
|
|
115
|
+
}
|
|
116
|
+
while ((yield* FiberSet.size(fs)) > 0) yield* Effect.sleep("250 millis")
|
|
117
|
+
})
|
|
118
|
+
const r = yield* sessionId
|
|
119
|
+
? makeSession(
|
|
120
|
+
sessionId,
|
|
121
|
+
wait
|
|
122
|
+
)
|
|
123
|
+
: make(wait)
|
|
124
|
+
|
|
125
|
+
const runEffect = <E>(effect: Effect<void, E, RMsg | RErr>) =>
|
|
126
|
+
new Promise<void>((resolve, reject) =>
|
|
127
|
+
fr(effect)
|
|
128
|
+
.addObserver((exit) => {
|
|
129
|
+
if (Exit.isSuccess(exit)) {
|
|
130
|
+
resolve(exit.value)
|
|
131
|
+
} else {
|
|
132
|
+
// disable @typescript-eslint/prefer-promise-reject-errors
|
|
133
|
+
reject(Cause.pretty(exit.cause, { renderErrorCause: true }))
|
|
134
|
+
}
|
|
135
|
+
})
|
|
136
|
+
)
|
|
137
|
+
yield* Effect.acquireRelease(
|
|
138
|
+
Effect
|
|
139
|
+
.sync(() =>
|
|
140
|
+
r
|
|
141
|
+
.subscribe({
|
|
142
|
+
processError: (err) =>
|
|
143
|
+
runEffect(
|
|
144
|
+
hndlr
|
|
145
|
+
.processError(err)
|
|
146
|
+
.pipe(
|
|
147
|
+
Effect.catchAllCause((cause) => Effect.logError(`ServiceBus Error ${sessionId}`, cause))
|
|
148
|
+
)
|
|
149
|
+
),
|
|
150
|
+
processMessage: (msg) => runEffect(hndlr.processMessage(msg))
|
|
151
|
+
// DO NOT CATCH ERRORS here as they should return to the queue!
|
|
152
|
+
})
|
|
153
|
+
)
|
|
154
|
+
.pipe(withSpanAndLog(`ServiceBus.subscription.create ${sessionId}`)),
|
|
155
|
+
(subscription) =>
|
|
156
|
+
Effect.promise(() => subscription.close()).pipe(
|
|
157
|
+
withSpanAndLog(`ServiceBus.subscription.close ${sessionId}`)
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
})
|
|
161
|
+
}
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
export class Receiver extends Context.TagId("Receiver")<Receiver, {
|
|
165
|
+
name: string
|
|
67
166
|
make: (waitTillEmpty: Effect<void>) => Effect<ServiceBusReceiver, never, Scope>
|
|
68
167
|
makeSession: (sessionId: string, waitTillEmpty: Effect<void>) => Effect<ServiceBusReceiver, never, Scope>
|
|
168
|
+
subscribe<RMsg, RErr>(
|
|
169
|
+
hndlr: MessageHandlers<RMsg, RErr>,
|
|
170
|
+
sessionId?: string
|
|
171
|
+
): Effect.Effect<void, never, Scope.Scope | RMsg | RErr>
|
|
69
172
|
}>() {
|
|
70
|
-
static readonly
|
|
71
|
-
this.toLayer(Client.pipe(Effect.andThen((cl) => ({
|
|
72
|
-
make: (waitTillEmpty: Effect<void>) =>
|
|
73
|
-
makeReceiver(queueName, waitTillEmpty).pipe(Effect.provideService(Client, cl)),
|
|
74
|
-
makeSession: (sessionId: string, waitTillEmpty: Effect<void>) =>
|
|
75
|
-
makeReceiver(queueName, waitTillEmpty, sessionId).pipe(Effect.provideService(Client, cl))
|
|
76
|
-
}))))
|
|
173
|
+
static readonly layer = (name: string) => this.toLayer(makeReceiver(name))
|
|
77
174
|
}
|
|
78
175
|
|
|
79
|
-
export
|
|
80
|
-
|
|
81
|
-
options?: OperationOptionsBase
|
|
82
|
-
) {
|
|
83
|
-
return Effect.gen(function*() {
|
|
84
|
-
const s = yield* Sender
|
|
85
|
-
return yield* Effect.promise(() => s.sendMessages(messages, options))
|
|
86
|
-
})
|
|
87
|
-
}
|
|
176
|
+
export const ReceiverTag = <Id>() => <Key extends string>(queueName: Key) => {
|
|
177
|
+
const tag = Context.Tag(`ServiceBus.Receiver.${queueName}`)<Id, Receiver>()
|
|
88
178
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
const fr = yield* FiberSet.runtime(fs)<RMsg | RErr>()
|
|
94
|
-
const wait = Effect
|
|
95
|
-
.gen(function*() {
|
|
96
|
-
if ((yield* FiberSet.size(fs)) > 0) {
|
|
97
|
-
yield* InfraLogger.logDebug("Waiting ServiceBusFiberSet to be empty: " + (yield* FiberSet.size(fs)))
|
|
98
|
-
}
|
|
99
|
-
while ((yield* FiberSet.size(fs)) > 0) yield* Effect.sleep("250 millis")
|
|
100
|
-
})
|
|
101
|
-
const r = yield* sessionId
|
|
102
|
-
? rf.makeSession(
|
|
103
|
-
sessionId,
|
|
104
|
-
wait
|
|
105
|
-
)
|
|
106
|
-
: rf.make(wait)
|
|
107
|
-
|
|
108
|
-
const runEffect = <E>(effect: Effect<void, E, RMsg | RErr>) =>
|
|
109
|
-
new Promise<void>((resolve, reject) =>
|
|
110
|
-
fr(effect)
|
|
111
|
-
.addObserver((exit) => {
|
|
112
|
-
if (Exit.isSuccess(exit)) {
|
|
113
|
-
resolve(exit.value)
|
|
114
|
-
} else {
|
|
115
|
-
// disable @typescript-eslint/prefer-promise-reject-errors
|
|
116
|
-
reject(Cause.pretty(exit.cause, { renderErrorCause: true }))
|
|
117
|
-
}
|
|
118
|
-
})
|
|
119
|
-
)
|
|
120
|
-
yield* Effect.acquireRelease(
|
|
121
|
-
Effect
|
|
122
|
-
.sync(() =>
|
|
123
|
-
r
|
|
124
|
-
.subscribe({
|
|
125
|
-
processError: (err) =>
|
|
126
|
-
runEffect(
|
|
127
|
-
hndlr
|
|
128
|
-
.processError(err)
|
|
129
|
-
.pipe(Effect.catchAllCause((cause) => Effect.logError(`ServiceBus Error ${sessionId}`, cause)))
|
|
130
|
-
),
|
|
131
|
-
processMessage: (msg) => runEffect(hndlr.processMessage(msg))
|
|
132
|
-
// DO NOT CATCH ERRORS here as they should return to the queue!
|
|
133
|
-
})
|
|
134
|
-
)
|
|
135
|
-
.pipe(withSpanAndLog(`ServiceBus.subscription.create ${sessionId}`)),
|
|
136
|
-
(subscription) =>
|
|
137
|
-
Effect.promise(() => subscription.close()).pipe(withSpanAndLog(`ServiceBus.subscription.close ${sessionId}`))
|
|
179
|
+
return Object.assign(tag, {
|
|
180
|
+
layer: Layer.effect(
|
|
181
|
+
tag,
|
|
182
|
+
makeReceiver(queueName).pipe(Effect.map((_) => Receiver.of(_)))
|
|
138
183
|
)
|
|
139
184
|
})
|
|
140
185
|
}
|
|
141
186
|
|
|
187
|
+
export const SenderReceiver = (queue: string, queueDrain?: string) =>
|
|
188
|
+
Layer.mergeAll(Sender.layer(queue), Receiver.layer(queueDrain ?? queue))
|
|
189
|
+
|
|
142
190
|
export interface MessageHandlers<RMsg, RErr> {
|
|
143
191
|
/**
|
|
144
192
|
* Handler that processes messages from service bus.
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { type AnyWithProps } from "@effect/rpc/Rpc"
|
|
3
3
|
import { Context, type Effect, type NonEmptyArray, type NonEmptyReadonlyArray, S } from "effect-app"
|
|
4
4
|
import { type GetContextConfig, type RPCContextMap } from "effect-app/client"
|
|
5
|
+
import { type TypeTestId } from "../../routing.js"
|
|
5
6
|
import { type MiddlewareMaker, middlewareMaker } from "./generic-middleware.js"
|
|
6
7
|
import { type AnyDynamic, type RpcDynamic, Tag, type TagClassAny } from "./RpcMiddleware.js"
|
|
7
8
|
|
|
@@ -103,7 +104,7 @@ export interface BuildingMiddleware<
|
|
|
103
104
|
: never
|
|
104
105
|
|
|
105
106
|
// helps debugging what are the missing requirements (type only)
|
|
106
|
-
|
|
107
|
+
readonly [TypeTestId]: {
|
|
107
108
|
missingDynamicMiddlewares: Exclude<keyof RequestContextMap, Provided>
|
|
108
109
|
missingContext: MiddlewareR
|
|
109
110
|
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
// codegen:start {preset: barrel, include: ./middleware/*.ts, nodir: false }
|
|
2
|
-
export * from "./middleware/dynamic-middleware.js"
|
|
3
2
|
export * from "./middleware/generic-middleware.js"
|
|
4
3
|
export * from "./middleware/middleware-api.js"
|
|
5
4
|
export * from "./middleware/middleware.js"
|