@effect/cluster 0.52.5 → 0.52.7

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,32 @@
1
+ import * as Cause from "effect/Cause";
2
+ import * as Effect from "effect/Effect";
3
+ /** @internal */
4
+ export const joinAllDiscard = fibers => Effect.async(resume => {
5
+ let cause = undefined;
6
+ let i = 0;
7
+ function loop() {
8
+ while (i < fibers.length) {
9
+ const fiber = fibers[i];
10
+ const exit = fiber.unsafePoll();
11
+ if (exit) {
12
+ i++;
13
+ if (exit._tag === "Success") continue;
14
+ cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause;
15
+ continue;
16
+ }
17
+ fiber.addObserver(onExit);
18
+ return;
19
+ }
20
+ resume(cause ? Effect.failCause(cause) : Effect.void);
21
+ }
22
+ function onExit(exit) {
23
+ i++;
24
+ if (exit._tag === "Failure") {
25
+ cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause;
26
+ }
27
+ loop();
28
+ }
29
+ loop();
30
+ return Effect.sync(() => fibers[i].removeObserver(onExit));
31
+ });
32
+ //# sourceMappingURL=fiber.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"fiber.js","names":["Cause","Effect","joinAllDiscard","fibers","async","resume","cause","undefined","i","loop","length","fiber","exit","unsafePoll","_tag","parallel","addObserver","onExit","failCause","void","sync","removeObserver"],"sources":["../../../src/internal/fiber.ts"],"sourcesContent":[null],"mappings":"AAAA,OAAO,KAAKA,KAAK,MAAM,cAAc;AACrC,OAAO,KAAKC,MAAM,MAAM,eAAe;AAIvC;AACA,OAAO,MAAMC,cAAc,GAAUC,MAA+C,IAClFF,MAAM,CAACG,KAAK,CAAWC,MAAM,IAAI;EAC/B,IAAIC,KAAK,GAA+BC,SAAS;EACjD,IAAIC,CAAC,GAAG,CAAC;EACT,SAASC,IAAIA,CAAA;IACX,OAAOD,CAAC,GAAGL,MAAM,CAACO,MAAM,EAAE;MACxB,MAAMC,KAAK,GAAGR,MAAM,CAACK,CAAC,CAAC;MACvB,MAAMI,IAAI,GAAGD,KAAK,CAACE,UAAU,EAAE;MAC/B,IAAID,IAAI,EAAE;QACRJ,CAAC,EAAE;QACH,IAAII,IAAI,CAACE,IAAI,KAAK,SAAS,EAAE;QAC7BR,KAAK,GAAGA,KAAK,GAAGN,KAAK,CAACe,QAAQ,CAACT,KAAK,EAAEM,IAAI,CAACN,KAAK,CAAC,GAAGM,IAAI,CAACN,KAAK;QAC9D;MACF;MACAK,KAAK,CAACK,WAAW,CAACC,MAAM,CAAC;MACzB;IACF;IACAZ,MAAM,CAACC,KAAK,GAAGL,MAAM,CAACiB,SAAS,CAACZ,KAAK,CAAC,GAAGL,MAAM,CAACkB,IAAI,CAAC;EACvD;EACA,SAASF,MAAMA,CAACL,IAAqB;IACnCJ,CAAC,EAAE;IACH,IAAII,IAAI,CAACE,IAAI,KAAK,SAAS,EAAE;MAC3BR,KAAK,GAAGA,KAAK,GAAGN,KAAK,CAACe,QAAQ,CAACT,KAAK,EAAEM,IAAI,CAACN,KAAK,CAAC,GAAGM,IAAI,CAACN,KAAK;IAChE;IACAG,IAAI,EAAE;EACR;EACAA,IAAI,EAAE;EACN,OAAOR,MAAM,CAACmB,IAAI,CAAC,MAAMjB,MAAM,CAACK,CAAC,CAAC,CAACa,cAAc,CAACJ,MAAM,CAAC,CAAC;AAC5D,CAAC,CAAC","ignoreList":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@effect/cluster",
3
- "version": "0.52.5",
3
+ "version": "0.52.7",
4
4
  "description": "Unified interfaces for common cluster-specific services",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -12,10 +12,10 @@
12
12
  "homepage": "https://effect.website",
13
13
  "peerDependencies": {
14
14
  "@effect/platform": "^0.93.0",
15
+ "@effect/rpc": "^0.72.1",
15
16
  "@effect/sql": "^0.48.0",
16
17
  "@effect/workflow": "^0.12.2",
17
- "@effect/rpc": "^0.72.1",
18
- "effect": "^3.19.2"
18
+ "effect": "^3.19.3"
19
19
  },
20
20
  "publishConfig": {
21
21
  "provenance": true
package/src/Sharding.ts CHANGED
@@ -8,12 +8,12 @@ import * as Arr from "effect/Array"
8
8
  import * as Cause from "effect/Cause"
9
9
  import * as Context from "effect/Context"
10
10
  import type { DurationInput } from "effect/Duration"
11
+ import * as Duration from "effect/Duration"
11
12
  import * as Effect from "effect/Effect"
12
13
  import * as Either from "effect/Either"
13
14
  import * as Equal from "effect/Equal"
14
15
  import type * as Exit from "effect/Exit"
15
16
  import * as Fiber from "effect/Fiber"
16
- import * as FiberHandle from "effect/FiberHandle"
17
17
  import * as FiberMap from "effect/FiberMap"
18
18
  import * as FiberRef from "effect/FiberRef"
19
19
  import * as FiberSet from "effect/FiberSet"
@@ -42,6 +42,7 @@ import { make as makeEntityId } from "./EntityId.js"
42
42
  import * as Envelope from "./Envelope.js"
43
43
  import * as EntityManager from "./internal/entityManager.js"
44
44
  import { EntityReaper } from "./internal/entityReaper.js"
45
+ import { joinAllDiscard } from "./internal/fiber.js"
45
46
  import { hashString } from "./internal/hash.js"
46
47
  import { internalInterruptors } from "./internal/interruptors.js"
47
48
  import { ResourceMap } from "./internal/resourceMap.js"
@@ -260,36 +261,43 @@ const make = Effect.gen(function*() {
260
261
  return Effect.ignore(runnerStorage.releaseAll(selfAddress))
261
262
  })
262
263
 
263
- const releaseShardsHandle = yield* FiberHandle.make()
264
- const releaseShards = Effect.suspend(function loop(): Effect.Effect<void, PersistenceError> {
265
- return Effect.flatMap(
266
- Effect.forEach(
267
- releasingShards,
268
- (shardId) =>
269
- Effect.forEach(
270
- entityManagers.values(),
271
- (state) => state.status === "closed" ? Effect.void : state.manager.interruptShard(shardId),
272
- { concurrency: "unbounded", discard: true }
273
- ).pipe(
274
- Effect.andThen(runnerStorage.release(selfAddress, shardId)),
275
- Effect.annotateLogs({ runner: selfAddress }),
276
- Effect.flatMap(() => {
277
- MutableHashSet.remove(releasingShards, shardId)
278
- return storage.unregisterShardReplyHandlers(shardId)
279
- })
280
- ),
281
- { concurrency: "unbounded", discard: true }
282
- ),
283
- () => {
284
- if (MutableHashSet.size(releasingShards) === 0) {
285
- return Effect.void
286
- }
287
- return loop()
264
+ const releaseShardsMap = yield* FiberMap.make<ShardId>()
265
+ const releaseShard = Effect.fnUntraced(
266
+ function*(shardId: ShardId) {
267
+ const fibers = Arr.empty<Fiber.RuntimeFiber<void>>()
268
+ for (const state of entityManagers.values()) {
269
+ if (state.status === "closed") continue
270
+ fibers.push(yield* Effect.fork(state.manager.interruptShard(shardId)))
288
271
  }
289
- )
290
- }).pipe(
291
- FiberHandle.run(releaseShardsHandle, { onlyIfMissing: true })
272
+ yield* joinAllDiscard(fibers)
273
+ yield* runnerStorage.release(selfAddress, shardId)
274
+ MutableHashSet.remove(releasingShards, shardId)
275
+ yield* storage.unregisterShardReplyHandlers(shardId)
276
+ },
277
+ Effect.sandbox,
278
+ (effect, shardId) =>
279
+ effect.pipe(
280
+ Effect.tapError((cause) =>
281
+ Effect.logDebug(`Could not release shard, retrying`, cause).pipe(
282
+ Effect.annotateLogs({
283
+ package: "@effect/cluster",
284
+ module: "Sharding",
285
+ fiber: "releaseShard",
286
+ runner: selfAddress,
287
+ shardId
288
+ })
289
+ )
290
+ ),
291
+ Effect.eventually,
292
+ FiberMap.run(releaseShardsMap, shardId, { onlyIfMissing: true })
293
+ )
292
294
  )
295
+ const releaseShards = Effect.gen(function*() {
296
+ for (const shardId of releasingShards) {
297
+ if (FiberMap.unsafeHas(releaseShardsMap, shardId)) continue
298
+ yield* releaseShard(shardId)
299
+ }
300
+ })
293
301
 
294
302
  yield* Effect.gen(function*() {
295
303
  activeShardsLatch.unsafeOpen()
@@ -873,16 +881,17 @@ const make = Effect.gen(function*() {
873
881
  const hashRings = new Map<string, HashRing.HashRing<RunnerAddress>>()
874
882
  let nextRunners = MutableHashMap.empty<Runner, boolean>()
875
883
  const healthyRunners = MutableHashSet.empty<Runner>()
884
+ const withTimeout = Effect.timeout(Duration.seconds(5))
876
885
 
877
886
  while (true) {
878
887
  // Ensure the current runner is registered
879
888
  if (selfRunner && !isShutdown.current && !MutableHashMap.has(allRunners, selfRunner)) {
880
889
  yield* Effect.logDebug("Registering runner", selfRunner)
881
- const machineId = yield* runnerStorage.register(selfRunner, true)
890
+ const machineId = yield* withTimeout(runnerStorage.register(selfRunner, true))
882
891
  yield* snowflakeGen.setMachineId(machineId)
883
892
  }
884
893
 
885
- const runners = yield* runnerStorage.getRunners
894
+ const runners = yield* withTimeout(runnerStorage.getRunners)
886
895
  let changed = false
887
896
  for (let i = 0; i < runners.length; i++) {
888
897
  const [runner, healthy] = runners[i]
@@ -965,7 +974,7 @@ const make = Effect.gen(function*() {
965
974
  yield* Effect.logWarning("No healthy runners available")
966
975
  // to prevent a deadlock, we will mark the current node as healthy to
967
976
  // start the health check singleton again
968
- yield* runnerStorage.setRunnerHealth(selfRunner.address, true)
977
+ yield* withTimeout(runnerStorage.setRunnerHealth(selfRunner.address, true))
969
978
  }
970
979
 
971
980
  yield* Effect.sleep(config.refreshAssignmentsInterval)
@@ -1099,8 +1108,8 @@ const make = Effect.gen(function*() {
1099
1108
 
1100
1109
  yield* Scope.addFinalizer(
1101
1110
  yield* Effect.scope,
1102
- Effect.withFiberRuntime((fiber) => {
1103
- internalInterruptors.add(fiber.id())
1111
+ Effect.fiberIdWith((fiberId) => {
1112
+ internalInterruptors.add(fiberId)
1104
1113
  return Effect.void
1105
1114
  })
1106
1115
  )
@@ -1368,8 +1377,7 @@ const make = Effect.gen(function*() {
1368
1377
  )
1369
1378
  }
1370
1379
 
1371
- const fiberId = yield* Effect.fiberId
1372
- internalInterruptors.add(fiberId)
1380
+ internalInterruptors.add(yield* Effect.fiberId)
1373
1381
  if (isShutdown.current) return
1374
1382
 
1375
1383
  MutableRef.set(isShutdown, true)
@@ -8,8 +8,9 @@ import * as Arr from "effect/Array"
8
8
  import * as Duration from "effect/Duration"
9
9
  import * as Effect from "effect/Effect"
10
10
  import * as Layer from "effect/Layer"
11
- import * as Resource from "effect/Resource"
11
+ import * as Scope from "effect/Scope"
12
12
  import { PersistenceError } from "./ClusterError.js"
13
+ import { ResourceRef } from "./internal/resourceRef.js"
13
14
  import * as RunnerStorage from "./RunnerStorage.js"
14
15
  import * as ShardId from "./ShardId.js"
15
16
  import * as ShardingConfig from "./ShardingConfig.js"
@@ -27,22 +28,27 @@ export const make = Effect.fnUntraced(function*(options: {
27
28
  const sql = (yield* SqlClient.SqlClient).withoutTransforms()
28
29
  const prefix = options?.prefix ?? "cluster"
29
30
  const table = (name: string) => `${prefix}_${name}`
31
+
30
32
  const acquireLockConn = sql.onDialectOrElse({
31
33
  pg: () =>
32
- Effect.gen(function*() {
33
- const conn = yield* Effect.orDie(sql.reserve)
34
- yield* Effect.addFinalizer(() => Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", [])))
34
+ Effect.fnUntraced(function*(scope: Scope.Scope) {
35
+ const conn = yield* Effect.orDie(sql.reserve).pipe(
36
+ Scope.extend(scope)
37
+ )
38
+ yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT pg_advisory_unlock_all()", [])))
35
39
  return conn
36
40
  }),
37
41
  mysql: () =>
38
- Effect.gen(function*() {
39
- const conn = yield* Effect.orDie(sql.reserve)
40
- yield* Effect.addFinalizer(() => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", [])))
42
+ Effect.fnUntraced(function*(scope: Scope.Scope) {
43
+ const conn = yield* Effect.orDie(sql.reserve).pipe(
44
+ Scope.extend(scope)
45
+ )
46
+ yield* Scope.addFinalizerExit(scope, () => Effect.orDie(conn.executeRaw("SELECT RELEASE_ALL_LOCKS()", [])))
41
47
  return conn
42
48
  }),
43
49
  orElse: () => undefined
44
50
  })
45
- const lockConnRef = acquireLockConn && (yield* Resource.manual(acquireLockConn))
51
+ const lockConn = acquireLockConn && (yield* ResourceRef.from(yield* Effect.scope, acquireLockConn))
46
52
 
47
53
  const runnersTable = table("runners")
48
54
  const runnersTableSql = sql(runnersTable)
@@ -206,28 +212,28 @@ export const make = Effect.fnUntraced(function*(options: {
206
212
  })
207
213
 
208
214
  const execWithLockConn = <A>(effect: Statement.Statement<A>): Effect.Effect<unknown, SqlError> => {
209
- if (!lockConnRef) return effect
215
+ if (!lockConn) return effect
210
216
  const [query, params] = effect.compile()
211
- return Resource.get(lockConnRef).pipe(
217
+ return lockConn.await.pipe(
212
218
  Effect.flatMap((conn) => conn.executeRaw(query, params)),
213
- Effect.onError(() => Resource.refresh(lockConnRef!))
219
+ Effect.onError(() => lockConn.unsafeRebuild())
214
220
  )
215
221
  }
216
222
  const execWithLockConnValues = <A>(
217
223
  effect: Statement.Statement<A>
218
224
  ): Effect.Effect<ReadonlyArray<ReadonlyArray<any>>, SqlError> => {
219
- if (!lockConnRef) return effect.values
225
+ if (!lockConn) return effect.values
220
226
  const [query, params] = effect.compile()
221
- return Resource.get(lockConnRef).pipe(
227
+ return lockConn.await.pipe(
222
228
  Effect.flatMap((conn) => conn.executeValues(query, params)),
223
- Effect.onError(() => Resource.refresh(lockConnRef!))
229
+ Effect.onError(() => lockConn.unsafeRebuild())
224
230
  )
225
231
  }
226
232
 
227
233
  const acquireLock = sql.onDialectOrElse({
228
234
  pg: () =>
229
235
  Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
230
- const conn = yield* Resource.get(lockConnRef!)
236
+ const conn = yield* lockConn!.await
231
237
  const acquiredShardIds: Array<string> = []
232
238
  const toAcquire = new Map(shardIds.map((shardId) => [lockNumbers.get(shardId)!, shardId]))
233
239
  const takenLocks = yield* conn.executeValues(
@@ -250,11 +256,11 @@ export const make = Effect.fnUntraced(function*(options: {
250
256
  }
251
257
  }
252
258
  return acquiredShardIds
253
- }, Effect.onError(() => Resource.refresh(lockConnRef!))),
259
+ }, Effect.onError(() => lockConn!.unsafeRebuild())),
254
260
 
255
261
  mysql: () =>
256
262
  Effect.fnUntraced(function*(_address: string, shardIds: ReadonlyArray<string>) {
257
- const conn = yield* Resource.get(lockConnRef!)
263
+ const conn = yield* lockConn!.await
258
264
  const takenLocks = (yield* conn.executeUnprepared(`SELECT ${allMySqlTakenLocks}`, [], undefined))[0] as Record<
259
265
  string,
260
266
  1 | null
@@ -281,7 +287,7 @@ export const make = Effect.fnUntraced(function*(options: {
281
287
  }
282
288
  }
283
289
  return acquiredShardIds
284
- }, Effect.onError(() => Resource.refresh(lockConnRef!))),
290
+ }, Effect.onError(() => lockConn!.unsafeRebuild())),
285
291
 
286
292
  mssql: () => (address: string, shardIds: ReadonlyArray<string>) => {
287
293
  const values = shardIds.map((shardId) => sql`(${stringLiteral(shardId)}, ${stringLiteral(address)}, ${sqlNow})`)
@@ -446,48 +452,43 @@ export const make = Effect.fnUntraced(function*(options: {
446
452
  Effect.fnUntraced(
447
453
  function*(_address, shardId) {
448
454
  const lockNum = lockNumbers.get(shardId)!
449
- const conn = yield* Resource.get(lockConnRef!)
450
- const release = conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, [])
451
- const check = conn.executeValues(
452
- `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`,
453
- []
454
- )
455
- while (true) {
456
- yield* release
457
- const takenLocks = yield* check
455
+ for (let i = 0; i < 5; i++) {
456
+ const conn = yield* lockConn!.await
457
+ yield* conn.executeRaw(`SELECT pg_advisory_unlock(${lockNum})`, [])
458
+ const takenLocks = yield* conn.executeValues(
459
+ `SELECT 1 FROM pg_locks WHERE locktype = 'advisory' AND granted = true AND pid = pg_backend_pid() AND objid = ${lockNum}`,
460
+ []
461
+ )
458
462
  if (takenLocks.length === 0) return
459
463
  }
464
+ const conn = yield* lockConn!.await
465
+ yield* conn.executeRaw(`SELECT pg_advisory_unlock_all()`, [])
460
466
  },
461
- Effect.onError(() => Resource.refresh(lockConnRef!)),
467
+ Effect.onError(() => lockConn!.unsafeRebuild()),
462
468
  Effect.asVoid,
463
- PersistenceError.refail,
464
- withTracerDisabled
469
+ PersistenceError.refail
465
470
  ),
466
471
  mysql: () =>
467
472
  Effect.fnUntraced(
468
473
  function*(_address, shardId) {
469
- const conn = yield* Resource.get(lockConnRef!)
470
474
  const lockName = lockNames.get(shardId)!
471
- const release = conn.executeRaw(`SELECT RELEASE_LOCK('${lockName}')`, [])
472
- const check = conn.executeValues(
473
- `SELECT IS_USED_LOCK('${lockName}') = CONNECTION_ID() AS is_taken`,
474
- []
475
- )
476
475
  while (true) {
477
- yield* release
478
- const takenLocks = yield* check
476
+ const conn = yield* lockConn!.await
477
+ yield* conn.executeRaw(`SELECT RELEASE_LOCK('${lockName}')`, [])
478
+ const takenLocks = yield* conn.executeValues(
479
+ `SELECT IS_USED_LOCK('${lockName}') = CONNECTION_ID() AS is_taken`,
480
+ []
481
+ )
479
482
  if (takenLocks.length === 0 || takenLocks[0][0] !== 1) return
480
483
  }
481
484
  },
482
- Effect.onError(() => Resource.refresh(lockConnRef!)),
485
+ Effect.onError(() => lockConn!.unsafeRebuild()),
483
486
  Effect.asVoid,
484
- PersistenceError.refail,
485
- withTracerDisabled
487
+ PersistenceError.refail
486
488
  ),
487
489
  orElse: () => (address, shardId) =>
488
490
  sql`DELETE FROM ${locksTableSql} WHERE address = ${address} AND shard_id = ${shardId}`.pipe(
489
- PersistenceError.refail,
490
- withTracerDisabled
491
+ PersistenceError.refail
491
492
  )
492
493
  }),
493
494
 
@@ -9,11 +9,13 @@ import type { DurationInput } from "effect/Duration"
9
9
  import * as Effect from "effect/Effect"
10
10
  import * as Equal from "effect/Equal"
11
11
  import * as Exit from "effect/Exit"
12
+ import type * as Fiber from "effect/Fiber"
12
13
  import * as FiberRef from "effect/FiberRef"
13
14
  import { identity } from "effect/Function"
14
15
  import * as HashMap from "effect/HashMap"
15
16
  import * as Metric from "effect/Metric"
16
17
  import * as Option from "effect/Option"
18
+ import * as Runtime from "effect/Runtime"
17
19
  import * as Schedule from "effect/Schedule"
18
20
  import * as Schema from "effect/Schema"
19
21
  import * as Scope from "effect/Scope"
@@ -34,6 +36,7 @@ import type { Sharding } from "../Sharding.js"
34
36
  import { ShardingConfig } from "../ShardingConfig.js"
35
37
  import * as Snowflake from "../Snowflake.js"
36
38
  import { EntityReaper } from "./entityReaper.js"
39
+ import { joinAllDiscard } from "./fiber.js"
37
40
  import { internalInterruptors } from "./interruptors.js"
38
41
  import { ResourceMap } from "./resourceMap.js"
39
42
  import { ResourceRef } from "./resourceRef.js"
@@ -434,35 +437,31 @@ export const make = Effect.fnUntraced(function*<
434
437
  )
435
438
  }
436
439
 
437
- const interruptShard = (shardId: ShardId) =>
438
- Effect.suspend(function loop(): Effect.Effect<void> {
439
- const toAwait = Arr.empty<Effect.Effect<void>>()
440
- activeServers.forEach((state) => {
441
- if (shardId[Equal.symbol](state.address.shardId)) {
442
- toAwait.push(entities.removeIgnore(state.address))
443
- }
444
- })
445
- serverCloseLatches.forEach((latch, address) => {
446
- if (shardId[Equal.symbol](address.shardId)) {
447
- toAwait.push(latch.await)
448
- }
449
- })
450
- if (toAwait.length === 0) {
451
- return Effect.void
452
- }
453
- return Effect.flatMap(
454
- Effect.all(toAwait, {
455
- concurrency: "unbounded",
456
- discard: true
457
- }),
458
- loop
459
- )
460
- })
461
-
462
440
  const decodeMessage = Schema.decode(makeMessageSchema(entity))
463
441
 
442
+ const runFork = Runtime.runFork(
443
+ yield* Effect.runtime<never>().pipe(
444
+ Effect.interruptible
445
+ )
446
+ )
447
+
464
448
  return identity<EntityManager>({
465
- interruptShard,
449
+ interruptShard: (shardId: ShardId) =>
450
+ Effect.suspend(function loop(): Effect.Effect<void> {
451
+ const fibers = Arr.empty<Fiber.RuntimeFiber<void>>()
452
+ activeServers.forEach((state) => {
453
+ if (shardId[Equal.symbol](state.address.shardId)) {
454
+ fibers.push(runFork(entities.removeIgnore(state.address)))
455
+ }
456
+ })
457
+ serverCloseLatches.forEach((latch, address) => {
458
+ if (shardId[Equal.symbol](address.shardId)) {
459
+ fibers.push(runFork(latch.await))
460
+ }
461
+ })
462
+ if (fibers.length === 0) return Effect.void
463
+ return Effect.flatMap(joinAllDiscard(fibers), loop)
464
+ }),
466
465
  isProcessingFor(message, options) {
467
466
  if (options?.excludeReplies !== true && processedRequestIds.has(message.envelope.requestId)) {
468
467
  return true
@@ -0,0 +1,35 @@
1
+ import * as Cause from "effect/Cause"
2
+ import * as Effect from "effect/Effect"
3
+ import type * as Exit from "effect/Exit"
4
+ import type * as Fiber from "effect/Fiber"
5
+
6
+ /** @internal */
7
+ export const joinAllDiscard = <A, E>(fibers: ReadonlyArray<Fiber.RuntimeFiber<A, E>>) =>
8
+ Effect.async<void, E>((resume) => {
9
+ let cause: Cause.Cause<E> | undefined = undefined
10
+ let i = 0
11
+ function loop() {
12
+ while (i < fibers.length) {
13
+ const fiber = fibers[i]
14
+ const exit = fiber.unsafePoll()
15
+ if (exit) {
16
+ i++
17
+ if (exit._tag === "Success") continue
18
+ cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause
19
+ continue
20
+ }
21
+ fiber.addObserver(onExit)
22
+ return
23
+ }
24
+ resume(cause ? Effect.failCause(cause) : Effect.void)
25
+ }
26
+ function onExit(exit: Exit.Exit<A, E>) {
27
+ i++
28
+ if (exit._tag === "Failure") {
29
+ cause = cause ? Cause.parallel(cause, exit.cause) : exit.cause
30
+ }
31
+ loop()
32
+ }
33
+ loop()
34
+ return Effect.sync(() => fibers[i].removeObserver(onExit))
35
+ })