@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.
Files changed (37) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/dist/MainFiberSet.d.ts +1 -1
  3. package/dist/Model/Repository/internal/internal.d.ts.map +1 -1
  4. package/dist/Model/Repository/internal/internal.js +38 -37
  5. package/dist/Operations.d.ts +1 -1
  6. package/dist/QueueMaker/sbqueue.d.ts +5 -6
  7. package/dist/QueueMaker/sbqueue.d.ts.map +1 -1
  8. package/dist/QueueMaker/sbqueue.js +18 -22
  9. package/dist/RequestFiberSet.d.ts +1 -1
  10. package/dist/Store/service.d.ts +1 -1
  11. package/dist/adapters/ServiceBus.d.ts +57 -17
  12. package/dist/adapters/ServiceBus.d.ts.map +1 -1
  13. package/dist/adapters/ServiceBus.js +75 -61
  14. package/dist/adapters/memQueue.d.ts +1 -1
  15. package/dist/api/routing/middleware/middleware-api.d.ts +2 -1
  16. package/dist/api/routing/middleware/middleware-api.d.ts.map +1 -1
  17. package/dist/api/routing/middleware/middleware-api.js +1 -1
  18. package/dist/api/routing/middleware.d.ts +0 -1
  19. package/dist/api/routing/middleware.d.ts.map +1 -1
  20. package/dist/api/routing/middleware.js +1 -2
  21. package/dist/api/routing.d.ts +8 -9
  22. package/dist/api/routing.d.ts.map +1 -1
  23. package/dist/api/routing.js +18 -10
  24. package/package.json +2 -6
  25. package/src/Model/Repository/internal/internal.ts +45 -41
  26. package/src/QueueMaker/sbqueue.ts +33 -46
  27. package/src/adapters/ServiceBus.ts +141 -93
  28. package/src/api/routing/middleware/middleware-api.ts +2 -1
  29. package/src/api/routing/middleware.ts +0 -1
  30. package/src/api/routing.ts +46 -233
  31. package/test/controller.test.ts +12 -8
  32. package/test/dist/controller.test.d.ts.map +1 -1
  33. package/test/dist/fixtures.d.ts +3 -3
  34. package/dist/api/routing/middleware/dynamic-middleware.d.ts +0 -2
  35. package/dist/api/routing/middleware/dynamic-middleware.d.ts.map +0 -1
  36. package/dist/api/routing/middleware/dynamic-middleware.js +0 -2
  37. 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
- function findEId(id: Encoded[IdKey]) {
130
- return Effect.flatMap(
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
- function findE(id: T[IdKey]) {
141
- return pipe(
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
- function find(id: T[IdKey]) {
150
- return Effect.flatMapOption(findE(id), (_) => Effect.orDie(decode(_)))
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
- return Effect
177
- .suspend(() => {
178
- const it = Chunk.fromIterable(items)
179
- return saveAll(it)
180
- .pipe(
181
- Effect.andThen(Effect.sync(() => toNonEmptyArray([...events]))),
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
- .pipe((_) => Effect.flatMapOption(_, pub))
189
+ (_) => Effect.flatMapOption(_, pub),
190
+ Effect.andThen(changeFeed.publish([Chunk.toArray(it), "save"])),
191
+ Effect.asVoid
192
+ )
193
+ })
205
194
 
206
- yield* changeFeed.publish([it, "remove"])
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, Layer, S } from "effect-app"
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 { LiveSender, LiveServiceBusClient, Sender, ServiceBusReceiverFactory, subscribe } from "../adapters/ServiceBus.js"
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 s = yield* Sender
32
- const receiver = yield* ServiceBusReceiverFactory
33
- const silenceAndReportError = reportNonInterruptedFailure({ name: "ServiceBusQueue.drain." + queueDrainName })
34
- const reportError = reportNonInterruptedFailureCause({ name: "ServiceBusQueue.drain." + queueDrainName })
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(`[${queueDrainName}] Processing incoming message`)
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: ${queueDrainName}${sessionId ? `#${sessionId}` : ""}.${body._tag}`,
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": queueDrainName,
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* subscribe({
99
- processMessage: (x) => processMessage(x.body).pipe(Effect.uninterruptible),
100
- processError: (err) => reportQueueError(Cause.fail(err.error))
101
- // Deferred.completeWith(
102
- // deferred,
103
- // reportFatalQueueError(Cause.fail(err.error))
104
- // .pipe(Effect.andThen(Effect.fail(err.error)))
105
- // )
106
- }, sessionId)
107
- .pipe(Effect.provideService(ServiceBusReceiverFactory, receiver))
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* Effect
119
- .promise((abortSignal) =>
120
- s.sendMessages(
121
- messages.map((m) => ({
122
- body: JSON.stringify(
123
- S.encodeSync(wireSchema)({
124
- body: m,
125
- meta: requestContext
126
- })
127
- ),
128
- messageId: m.id, /* correllationid: requestId */
129
- contentType: "application/json",
130
- sessionId: "sessionId" in m ? m.sessionId : undefined
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: " + queueName, {
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, type ServiceBusSender } from "@azure/service-bus"
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
- const Client = Context.GenericTag<ServiceBusClient>("@services/Client")
22
- export const LiveServiceBusClient = (url: string) => Layer.scoped(Client)(makeClient(url))
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 makeSender(queueName: string) {
25
+ function makeSender_(queueName: string) {
25
26
  return Effect.gen(function*() {
26
- const serviceBusClient = yield* Client
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
- export function LiveSender(queueName: string) {
39
- return Layer
40
- .scoped(Sender, makeSender(queueName))
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
- function makeReceiver(queueName: string, waitTillEmpty: Effect<void>, sessionId?: string) {
44
- return Effect.gen(function*() {
45
- const serviceBusClient = yield* Client
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
- return yield* Effect.acquireRelease(
48
- (sessionId
49
- ? Effect.promise(() => serviceBusClient.acceptSession(queueName, sessionId))
50
- : Effect.sync(() => serviceBusClient.createReceiver(queueName)))
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
- export class ServiceBusReceiverFactory extends Context.TagId(
65
- "ServiceBusReceiverFactory"
66
- )<ServiceBusReceiverFactory, {
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 Live = (queueName: string) =>
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 function sendMessages(
80
- messages: ServiceBusMessage | ServiceBusMessage[] | ServiceBusMessageBatch,
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
- export function subscribe<RMsg, RErr>(hndlr: MessageHandlers<RMsg, RErr>, sessionId?: string) {
90
- return Effect.gen(function*() {
91
- const rf = yield* ServiceBusReceiverFactory
92
- const fs = yield* FiberSet.make()
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
- missing: {
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"