import { assert, describe, it } from "@effect/vitest" import { Effect, Fiber, TxPubSub, TxQueue } from "effect" describe("TxPubSub", () => { describe("constructors", () => { it.effect("bounded creates pubsub with specified capacity", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.bounded(16)) assert.strictEqual(TxPubSub.capacity(hub), 16) assert.strictEqual(yield* Effect.tx(TxPubSub.isShutdown(hub)), false) })) it.effect("unbounded creates pubsub with unlimited capacity", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) assert.strictEqual(TxPubSub.capacity(hub), Number.POSITIVE_INFINITY) })) it.effect("dropping creates pubsub with dropping strategy", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.dropping(4)) assert.strictEqual(TxPubSub.capacity(hub), 4) })) it.effect("sliding creates pubsub with sliding strategy", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.sliding(4)) assert.strictEqual(TxPubSub.capacity(hub), 4) })) }) describe("publish and subscribe", () => { it.effect("publish with no subscribers is a no-op", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) const result = yield* Effect.tx(TxPubSub.publish(hub, "hello")) assert.strictEqual(result, true) })) it.effect("subscribe then publish then take", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, "hello")) const msg = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(msg, "hello") }) ) })) it.effect("multiple subscribers each receive all messages", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.scoped( Effect.gen(function*() { const sub1 = yield* TxPubSub.subscribe(hub) const sub2 = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) const v1a = yield* Effect.tx(TxQueue.take(sub1)) const v1b = yield* Effect.tx(TxQueue.take(sub1)) const v2a = yield* Effect.tx(TxQueue.take(sub2)) const v2b = yield* Effect.tx(TxQueue.take(sub2)) assert.strictEqual(v1a, 1) assert.strictEqual(v1b, 2) assert.strictEqual(v2a, 1) assert.strictEqual(v2b, 2) }) ) })) it.effect("publishAll delivers all messages", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publishAll(hub, [1, 2, 3])) const v1 = yield* Effect.tx(TxQueue.take(sub)) const v2 = yield* Effect.tx(TxQueue.take(sub)) const v3 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v1, 1) assert.strictEqual(v2, 2) assert.strictEqual(v3, 3) }) ) })) it.effect("subscriber only receives messages published after subscription", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) // Publish before subscription yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 2)) yield* Effect.tx(TxPubSub.publish(hub, 3)) const v1 = yield* Effect.tx(TxQueue.take(sub)) const v2 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v1, 2) assert.strictEqual(v2, 3) }) ) })) }) describe("strategies", () => { it.effect("bounded publisher blocks when subscriber queue is full", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.bounded(2)) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) // Publisher should block since subscriber queue is full const fiber = yield* Effect.forkChild(Effect.tx(TxPubSub.publish(hub, 3))) // Take one to make space, unblocking the publisher const v1 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v1, 1) // Publisher should now complete yield* Fiber.join(fiber) const v2 = yield* Effect.tx(TxQueue.take(sub)) const v3 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v2, 2) assert.strictEqual(v3, 3) }) ) })) it.effect("dropping strategy drops messages when subscriber queue is full", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.dropping(2)) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) const accepted = yield* Effect.tx(TxPubSub.publish(hub, 3)) // should be dropped assert.strictEqual(accepted, false) const v1 = yield* Effect.tx(TxQueue.take(sub)) const v2 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v1, 1) assert.strictEqual(v2, 2) }) ) })) it.effect("sliding strategy drops oldest messages when subscriber queue is full", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.sliding(2)) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) yield* Effect.tx(TxPubSub.publish(hub, 3)) // evicts 1 const v1 = yield* Effect.tx(TxQueue.take(sub)) const v2 = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v1, 2) assert.strictEqual(v2, 3) }) ) })) it.effect("unbounded strategy always accepts", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) for (let i = 0; i < 100; i++) { const accepted = yield* Effect.tx(TxPubSub.publish(hub, i)) assert.strictEqual(accepted, true) } for (let i = 0; i < 100; i++) { const v = yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(v, i) } }) ) })) }) describe("getters", () => { it.effect("size returns max subscriber queue size", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) assert.strictEqual(yield* Effect.tx(TxPubSub.size(hub)), 0) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) assert.strictEqual(yield* Effect.tx(TxPubSub.size(hub)), 2) yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(yield* Effect.tx(TxPubSub.size(hub)), 1) }) ) })) it.effect("isEmpty checks all subscriber queues are empty", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) assert.strictEqual(yield* Effect.tx(TxPubSub.isEmpty(hub)), true) yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) assert.strictEqual(yield* Effect.tx(TxPubSub.isEmpty(hub)), true) yield* Effect.tx(TxPubSub.publish(hub, 1)) assert.strictEqual(yield* Effect.tx(TxPubSub.isEmpty(hub)), false) yield* Effect.tx(TxQueue.take(sub)) assert.strictEqual(yield* Effect.tx(TxPubSub.isEmpty(hub)), true) }) ) })) it.effect("isFull checks if any subscriber queue is at capacity", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.bounded(2)) assert.strictEqual(yield* Effect.tx(TxPubSub.isFull(hub)), false) yield* Effect.scoped( Effect.gen(function*() { yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) assert.strictEqual(yield* Effect.tx(TxPubSub.isFull(hub)), false) yield* Effect.tx(TxPubSub.publish(hub, 2)) assert.strictEqual(yield* Effect.tx(TxPubSub.isFull(hub)), true) }) ) })) }) describe("shutdown", () => { it.effect("shutdown prevents further publishes", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.tx(TxPubSub.shutdown(hub)) assert.strictEqual(yield* Effect.tx(TxPubSub.isShutdown(hub)), true) const accepted = yield* Effect.tx(TxPubSub.publish(hub, 1)) assert.strictEqual(accepted, false) })) it.effect("shutdown is idempotent", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.tx(TxPubSub.shutdown(hub)) yield* Effect.tx(TxPubSub.shutdown(hub)) // second call should not throw assert.strictEqual(yield* Effect.tx(TxPubSub.isShutdown(hub)), true) })) it.effect("awaitShutdown blocks until shutdown", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) const fiber = yield* Effect.forkChild(Effect.tx(TxPubSub.awaitShutdown(hub))) yield* Effect.tx(TxPubSub.shutdown(hub)) yield* Fiber.join(fiber) })) it.effect("awaitShutdown returns immediately if already shutdown", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.tx(TxPubSub.shutdown(hub)) yield* Effect.tx(TxPubSub.awaitShutdown(hub)) // should not block })) }) describe("scope cleanup", () => { it.effect("closing scope removes subscriber", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) // Subscribe inside a scope then close it yield* Effect.scoped( Effect.gen(function*() { yield* TxPubSub.subscribe(hub) }) ) // Publish should succeed with no subscribers const result = yield* Effect.tx(TxPubSub.publish(hub, 1)) assert.strictEqual(result, true) assert.strictEqual(yield* Effect.tx(TxPubSub.size(hub)), 0) })) it.effect("subscriber receives messages only while scope is open", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) const results: Array = [] yield* Effect.scoped( Effect.gen(function*() { const sub = yield* TxPubSub.subscribe(hub) yield* Effect.tx(TxPubSub.publish(hub, 1)) yield* Effect.tx(TxPubSub.publish(hub, 2)) results.push(yield* Effect.tx(TxQueue.take(sub))) results.push(yield* Effect.tx(TxQueue.take(sub))) }) ) // After scope closes, new messages should not accumulate yield* Effect.tx(TxPubSub.publish(hub, 3)) assert.strictEqual(yield* Effect.tx(TxPubSub.size(hub)), 0) assert.deepStrictEqual(results, [1, 2]) })) }) describe("guards", () => { it.effect("isTxPubSub returns true for TxPubSub instances", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) assert.strictEqual(TxPubSub.isTxPubSub(hub), true) assert.strictEqual(TxPubSub.isTxPubSub({}), false) assert.strictEqual(TxPubSub.isTxPubSub(null), false) assert.strictEqual(TxPubSub.isTxPubSub(42), false) })) }) describe("concurrency", () => { it.effect("concurrent publishers and subscribers", () => Effect.gen(function*() { const hub = yield* Effect.tx(TxPubSub.unbounded()) yield* Effect.scoped( Effect.gen(function*() { const sub1 = yield* TxPubSub.subscribe(hub) const sub2 = yield* TxPubSub.subscribe(hub) // Fork publishers const f1 = yield* Effect.forkChild(Effect.tx(TxPubSub.publishAll(hub, [1, 2, 3]))) const f2 = yield* Effect.forkChild(Effect.tx(TxPubSub.publishAll(hub, [4, 5, 6]))) yield* Fiber.join(f1) yield* Fiber.join(f2) // Each subscriber should get all 6 messages const items1: Array = [] const items2: Array = [] for (let i = 0; i < 6; i++) { items1.push(yield* Effect.tx(TxQueue.take(sub1))) items2.push(yield* Effect.tx(TxQueue.take(sub2))) } assert.strictEqual(items1.length, 6) assert.strictEqual(items2.length, 6) // Both should have the same items (though order across publishers may vary) assert.deepStrictEqual(items1.sort(), [1, 2, 3, 4, 5, 6]) assert.deepStrictEqual(items2.sort(), [1, 2, 3, 4, 5, 6]) }) ) })) }) })