fix(bus): acquire PubSub subscription eagerly to close /event race (#27959)
This commit is contained in:
@@ -44,7 +44,7 @@ describe("Bus (Effect-native)", () => {
|
||||
const done = yield* Deferred.make<void>()
|
||||
const ready = yield* Latch.make()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.properties.value < 0) {
|
||||
yield* ready.open
|
||||
@@ -71,7 +71,7 @@ describe("Bus (Effect-native)", () => {
|
||||
const done = yield* Deferred.make<void>()
|
||||
const ready = yield* Latch.make()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.properties.value < 0) {
|
||||
yield* ready.open
|
||||
@@ -98,7 +98,7 @@ describe("Bus (Effect-native)", () => {
|
||||
const done = yield* Deferred.make<void>()
|
||||
const ready = yield* Latch.make()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribeAll(), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribeAll(), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.type === TestEvent.Warmup.type) {
|
||||
yield* ready.open
|
||||
@@ -129,7 +129,7 @@ describe("Bus (Effect-native)", () => {
|
||||
const readyA = yield* Latch.make()
|
||||
const readyB = yield* Latch.make()
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.properties.value < 0) {
|
||||
yield* readyA.open
|
||||
@@ -140,7 +140,7 @@ describe("Bus (Effect-native)", () => {
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Stream.runForEach(bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribe(TestEvent.Ping), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.properties.value < 0) {
|
||||
yield* readyB.open
|
||||
@@ -162,6 +162,92 @@ describe("Bus (Effect-native)", () => {
|
||||
}),
|
||||
)
|
||||
|
||||
// RACE 1: eager subscription means publishing immediately after yield*
|
||||
// bus.subscribe is delivered. Regression for the old lazy `Stream.unwrap`
|
||||
// shape where PubSub.subscribe ran on first pull and missed any publish
|
||||
// in the hand-off window.
|
||||
it.instance("eager subscribe: publish after yield* is delivered without consumer-activation race", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const stream = yield* bus.subscribe(TestEvent.Ping)
|
||||
|
||||
// Hand-off window: subscription is alive (we yielded). Publish goes
|
||||
// straight into the subscription queue, even with no consumer running.
|
||||
yield* bus.publish(TestEvent.Ping, { value: 99 })
|
||||
|
||||
const collected = yield* stream.pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect,
|
||||
Effect.timeout("400 millis"),
|
||||
Effect.option,
|
||||
)
|
||||
|
||||
expect(collected._tag).toBe("Some")
|
||||
if (collected._tag === "Some") {
|
||||
const arr = Array.from(collected.value)
|
||||
expect(arr[0].properties.value).toBe(99)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// RACE 2: same property for subscribeAll.
|
||||
it.instance("eager subscribeAll: publish after yield* is delivered", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const stream = yield* bus.subscribeAll()
|
||||
|
||||
yield* bus.publish(TestEvent.Ping, { value: 42 })
|
||||
|
||||
const collected = yield* stream.pipe(
|
||||
Stream.take(1),
|
||||
Stream.runCollect,
|
||||
Effect.timeout("400 millis"),
|
||||
Effect.option,
|
||||
)
|
||||
|
||||
expect(collected._tag).toBe("Some")
|
||||
if (collected._tag === "Some") {
|
||||
const arr = Array.from(collected.value)
|
||||
expect(arr[0].type).toBe(TestEvent.Ping.type)
|
||||
}
|
||||
}),
|
||||
)
|
||||
|
||||
// RACE 3: the /event-handler shape exactly. With eager subscription, the
|
||||
// bus subscription is alive before Stream.concat ever starts. Publishes
|
||||
// during the prefix consumption window are queued and delivered.
|
||||
it.instance("eager subscribe: Stream.concat(initial, subscribe) delivers publish during prefix", () =>
|
||||
Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
const sawInitial = yield* Deferred.make<void>()
|
||||
const sawPublish = yield* Deferred.make<number>()
|
||||
|
||||
type Frame = { marker?: "initial"; value?: number }
|
||||
const subscriptionStream = yield* bus.subscribe(TestEvent.Ping)
|
||||
const handlerStream: Stream.Stream<Frame> = Stream.make({ marker: "initial" } as Frame).pipe(
|
||||
Stream.concat(subscriptionStream.pipe(Stream.map((evt): Frame => ({ value: evt.properties.value })))),
|
||||
)
|
||||
|
||||
yield* Stream.runForEach(handlerStream, (frame) =>
|
||||
Effect.gen(function* () {
|
||||
if (frame.marker === "initial") {
|
||||
Deferred.doneUnsafe(sawInitial, Effect.void)
|
||||
return
|
||||
}
|
||||
if (frame.value !== undefined) Deferred.doneUnsafe(sawPublish, Effect.succeed(frame.value))
|
||||
}),
|
||||
).pipe(Effect.forkScoped)
|
||||
|
||||
yield* Deferred.await(sawInitial).pipe(Effect.timeout("1 second"))
|
||||
|
||||
yield* bus.publish(TestEvent.Ping, { value: 7 })
|
||||
|
||||
const got = yield* Deferred.await(sawPublish).pipe(Effect.timeout("1 second"), Effect.option)
|
||||
expect(got._tag).toBe("Some")
|
||||
if (got._tag === "Some") expect(got.value).toBe(7)
|
||||
}),
|
||||
)
|
||||
|
||||
it.live("subscribeAll stream sees InstanceDisposed on disposal", () =>
|
||||
Effect.gen(function* () {
|
||||
const dir = yield* tmpdirScoped()
|
||||
@@ -174,7 +260,7 @@ describe("Bus (Effect-native)", () => {
|
||||
yield* Effect.gen(function* () {
|
||||
const bus = yield* Bus.Service
|
||||
|
||||
yield* Stream.runForEach(bus.subscribeAll(), (evt) =>
|
||||
yield* Stream.runForEach(yield* bus.subscribeAll(), (evt) =>
|
||||
Effect.gen(function* () {
|
||||
if (evt.type === TestEvent.Warmup.type) {
|
||||
yield* ready.open
|
||||
|
||||
Reference in New Issue
Block a user