EdgeAIG's picture
download
raw
94.4 kB
/**
* Broadcasts values from publishers to many subscribers.
*
* Publishers add messages with `publish` or `publishAll`, and each active
* `Subscription` receives its own copy of every accepted message. Unlike a
* queue, subscribers do not compete for messages. This module includes bounded,
* dropping, sliding, and unbounded hubs, optional replay buffers for late
* subscribers, message-taking helpers, capacity and shutdown operations, and
* low-level types for custom hub strategies.
*
* @since 2.0.0
*/
import * as Arr from "./Array.ts"
import * as Context from "./Context.ts"
import * as Deferred from "./Deferred.ts"
import * as Effect from "./Effect.ts"
import * as Exit from "./Exit.ts"
import type { LazyArg } from "./Function.ts"
import { dual, identity } from "./Function.ts"
import * as Latch from "./Latch.ts"
import * as MutableList from "./MutableList.ts"
import * as MutableRef from "./MutableRef.ts"
import { nextPow2 } from "./Number.ts"
import * as Option from "./Option.ts"
import { type Pipeable, pipeArguments } from "./Pipeable.ts"
import * as Scope from "./Scope.ts"
import type { Covariant, Invariant } from "./Types.ts"
const TypeId = "~effect/PubSub"
/**
* A `PubSub<A>` is an asynchronous message hub into which publishers can publish
* messages of type `A` and subscribers can subscribe to take messages of type
* `A`.
*
* **Example** (Publishing and subscribing to messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create a bounded PubSub with capacity 10
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Subscribe and consume messages
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish messages
* yield* PubSub.publish(pubsub, "Hello")
* yield* PubSub.publish(pubsub, "World")
*
* const message1 = yield* PubSub.take(subscription)
* const message2 = yield* PubSub.take(subscription)
* console.log(message1, message2) // "Hello", "World"
* }))
* })
* ```
*
* @category models
* @since 2.0.0
*/
export interface PubSub<in out A> extends Pipeable {
readonly [TypeId]: {
readonly _A: Invariant<A>
}
readonly pubsub: PubSub.Atomic<A>
readonly subscribers: PubSub.Subscribers<A>
readonly scope: Scope.Closeable
readonly shutdownHook: Latch.Latch
readonly shutdownFlag: MutableRef.MutableRef<boolean>
readonly strategy: PubSub.Strategy<A>
}
/**
* Companion namespace containing the low-level building blocks used by
* `PubSub`, including atomic implementations, backing subscriptions, replay
* windows, and delivery strategies.
*
* @since 2.0.0
*/
export declare namespace PubSub {
/**
* Low-level atomic PubSub interface that handles the core message storage and retrieval.
*
* @category models
* @since 4.0.0
*/
export interface Atomic<in out A> {
readonly capacity: number
isEmpty(): boolean
isFull(): boolean
size(): number
publish(value: A): boolean
publishAll(elements: Iterable<A>): Array<A>
slide(): void
subscribe(): BackingSubscription<A>
replayWindow(): ReplayWindow<A>
}
/**
* Low-level subscription interface that handles message polling for individual subscribers.
*
* @category models
* @since 4.0.0
*/
export interface BackingSubscription<out A> {
isEmpty(): boolean
size(): number
poll(): A | MutableList.Empty
pollUpTo(n: number): Array<A>
unsubscribe(): void
}
/**
* Tracks the pollers currently waiting on each backing subscription.
*
* **Details**
*
* This type is part of the low-level `PubSub.Strategy` contract. Most
* application code should use `subscribe`, `take`, and the other `PubSub`
* operations instead of manipulating subscriber maps directly.
*
* @category models
* @since 4.0.0
*/
export type Subscribers<A> = Map<
BackingSubscription<A>,
Set<MutableList.MutableList<Deferred.Deferred<A>>>
>
/**
* Interface for accessing replay buffer contents for late subscribers.
*
* @category models
* @since 4.0.0
*/
export interface ReplayWindow<A> {
take(): A | undefined
takeN(n: number): Array<A>
takeAll(): Array<A>
readonly remaining: number
}
/**
* Strategy interface defining how PubSub handles backpressure and message distribution.
*
* @category models
* @since 4.0.0
*/
export interface Strategy<in out A> {
/**
* Describes any finalization logic associated with this strategy.
*/
readonly shutdown: Effect.Effect<void>
/**
* Describes how publishers should signal to subscribers that they are
* waiting for space to become available in the `PubSub`.
*/
handleSurplus(
pubsub: Atomic<A>,
subscribers: Subscribers<A>,
elements: Iterable<A>,
isShutdown: MutableRef.MutableRef<boolean>
): Effect.Effect<boolean>
/**
* Describes how subscribers should signal to publishers waiting for space
* to become available in the `PubSub` that space may be available.
*/
onPubSubEmptySpaceUnsafe(
pubsub: Atomic<A>,
subscribers: Subscribers<A>
): void
/**
* Describes how subscribers waiting for additional values from the `PubSub`
* should take those values and signal to publishers that they are no
* longer waiting for additional values.
*/
completePollersUnsafe(
pubsub: Atomic<A>,
subscribers: Subscribers<A>,
subscription: BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
): void
/**
* Describes how publishers should signal to subscribers waiting for
* additional values from the `PubSub` that new values are available.
*/
completeSubscribersUnsafe(
pubsub: Atomic<A>,
subscribers: Subscribers<A>
): void
}
}
const SubscriptionTypeId = "~effect/PubSub/Subscription"
/**
* A subscription represents a consumer's connection to a PubSub, allowing them to take messages.
*
* **Example** (Taking messages from a subscription)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Subscribe within a scope for automatic cleanup
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription: PubSub.Subscription<string> = yield* PubSub.subscribe(
* pubsub
* )
*
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Take individual messages
* const message = yield* PubSub.take(subscription)
* console.log(message) // "msg1"
*
* // Take multiple messages
* const messages = yield* PubSub.takeUpTo(subscription, 1)
* console.log(messages) // ["msg2"]
* const allMessages = yield* PubSub.takeAll(subscription)
* console.log(allMessages) // ["msg3"]
* }))
* })
* ```
*
* @category models
* @since 4.0.0
*/
export interface Subscription<out A> extends Pipeable {
readonly [SubscriptionTypeId]: {
readonly _A: Covariant<A>
}
readonly pubsub: PubSub.Atomic<any>
readonly subscribers: PubSub.Subscribers<any>
readonly subscription: PubSub.BackingSubscription<A>
readonly pollers: MutableList.MutableList<Deferred.Deferred<any>>
readonly shutdownHook: Latch.Latch
readonly shutdownFlag: MutableRef.MutableRef<boolean>
readonly strategy: PubSub.Strategy<any>
readonly replayWindow: PubSub.ReplayWindow<A>
}
/**
* Creates a PubSub with a custom atomic implementation and strategy.
*
* **Example** (Creating a PubSub with a custom strategy)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create custom PubSub with specific atomic implementation and strategy
* const pubsub = yield* PubSub.make<string>({
* atomicPubSub: () => PubSub.makeAtomicBounded(100),
* strategy: () => new PubSub.BackPressureStrategy()
* })
*
* // Use the created PubSub
* yield* PubSub.publish(pubsub, "Hello")
* })
* ```
*
* @category constructors
* @since 4.0.0
*/
export const make = <A>(
options: {
readonly atomicPubSub: LazyArg<PubSub.Atomic<A>>
readonly strategy: LazyArg<PubSub.Strategy<A>>
}
): Effect.Effect<PubSub<A>> =>
Effect.sync(() =>
makePubSubUnsafe(
options.atomicPubSub(),
new Map(),
Scope.makeUnsafe(),
Latch.makeUnsafe(false),
MutableRef.make(false),
options.strategy()
)
)
/**
* Creates a bounded `PubSub` that applies backpressure when it reaches
* capacity.
*
* **Details**
*
* Published messages are retained until all current subscribers have taken
* them. When the capacity is full, publishers suspend until space is available.
* Pass an options object to configure both `capacity` and an optional replay
* buffer for late subscribers.
*
* **Example** (Creating a bounded PubSub)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create bounded PubSub with capacity 100
* const pubsub = yield* PubSub.bounded<string>(100)
*
* // Create with replay buffer for late subscribers
* const pubsubWithReplay = yield* PubSub.bounded<string>({
* capacity: 100,
* replay: 10 // Last 10 messages replayed to new subscribers
* })
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const bounded = <A>(
capacity: number | {
readonly capacity: number
readonly replay?: number | undefined
}
): Effect.Effect<PubSub<A>> =>
make({
atomicPubSub: () => makeAtomicBounded(capacity),
strategy: () => new BackPressureStrategy()
})
/**
* Creates a bounded `PubSub` with the dropping strategy. The `PubSub` will drop new
* messages if the `PubSub` is at capacity.
*
* **Details**
*
* For best performance use capacities that are powers of two.
*
* **Example** (Dropping messages when full)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create dropping PubSub that drops new messages when full
* const pubsub = yield* PubSub.dropping<string>(3)
*
* // With replay buffer for late subscribers
* const pubsubWithReplay = yield* PubSub.dropping<string>({
* capacity: 3,
* replay: 5
* })
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Fill the PubSub and see dropping behavior
* yield* PubSub.publish(pubsub, "msg1") // succeeds
* yield* PubSub.publish(pubsub, "msg2") // succeeds
* yield* PubSub.publish(pubsub, "msg3") // succeeds
* const dropped = yield* PubSub.publish(pubsub, "msg4") // returns false (dropped)
* console.log("Message dropped:", !dropped) // true
*
* const messages = yield* PubSub.takeAll(subscription)
* console.log(messages) // ["msg1", "msg2", "msg3"]
* }))
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const dropping = <A>(
capacity: number | {
readonly capacity: number
readonly replay?: number | undefined
}
): Effect.Effect<PubSub<A>> =>
make({
atomicPubSub: () => makeAtomicBounded(capacity),
strategy: () => new DroppingStrategy()
})
/**
* Creates a bounded `PubSub` with the sliding strategy. The `PubSub` will add new
* messages and drop old messages if the `PubSub` is at capacity.
*
* **Details**
*
* For best performance use capacities that are powers of two.
*
* **Example** (Sliding old messages when full)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create sliding PubSub that evicts old messages when full
* const pubsub = yield* PubSub.sliding<string>(3)
*
* // With replay buffer
* const pubsubWithReplay = yield* PubSub.sliding<string>({
* capacity: 3,
* replay: 2
* })
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Fill and overflow the PubSub
* yield* PubSub.publish(pubsub, "msg1")
* yield* PubSub.publish(pubsub, "msg2")
* yield* PubSub.publish(pubsub, "msg3")
* yield* PubSub.publish(pubsub, "msg4") // "msg1" is evicted
*
* const messages = yield* PubSub.takeAll(subscription)
* console.log(messages) // ["msg2", "msg3", "msg4"]
* }))
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const sliding = <A>(
capacity: number | {
readonly capacity: number
readonly replay?: number | undefined
}
): Effect.Effect<PubSub<A>> =>
make({
atomicPubSub: () => makeAtomicBounded(capacity),
strategy: () => new SlidingStrategy()
})
/**
* Creates an unbounded `PubSub`.
*
* **Example** (Creating an unbounded PubSub)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create unbounded PubSub
* const pubsub = yield* PubSub.unbounded<string>()
*
* // With replay buffer for late subscribers
* const pubsubWithReplay = yield* PubSub.unbounded<string>({
* replay: 10
* })
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Can publish unlimited messages
* for (let i = 0; i < 3; i++) {
* yield* PubSub.publish(pubsub, `message-${i}`)
* }
*
* const message = yield* PubSub.take(subscription)
* console.log("First message:", message) // "message-0"
* }))
* })
* ```
*
* @category constructors
* @since 2.0.0
*/
export const unbounded = <A>(options?: {
readonly replay?: number | undefined
}): Effect.Effect<PubSub<A>> =>
make({
atomicPubSub: () => makeAtomicUnbounded(options),
strategy: () => new DroppingStrategy()
})
/**
* Creates a bounded atomic PubSub implementation with optional replay buffer.
*
* **When to use**
*
* Use to provide bounded message storage when building a custom `PubSub` with
* `make` and an explicit delivery strategy.
*
* **Details**
*
* Pass either a capacity number or an options object with `capacity` and
* optional `replay`. A positive `replay` value enables a replay buffer for late
* subscribers, and fractional replay sizes are rounded up.
*
* **Gotchas**
*
* The capacity must be greater than zero; invalid capacities throw
* synchronously before an atomic implementation is created.
*
* @see {@link make} for constructing a `PubSub` from an atomic implementation and delivery strategy
* @see {@link makeAtomicUnbounded} for an atomic implementation without a bounded capacity
* @see {@link bounded} for the higher-level backpressure constructor
* @see {@link dropping} for the higher-level dropping constructor
* @see {@link sliding} for the higher-level sliding constructor
*
* @category constructors
* @since 4.0.0
*/
export const makeAtomicBounded = <A>(
capacity: number | {
readonly capacity: number
readonly replay?: number | undefined
}
): PubSub.Atomic<A> => {
const options = typeof capacity === "number" ? { capacity } : capacity
ensureCapacity(options.capacity)
const replayBuffer = options.replay && options.replay > 0 ? new ReplayBuffer<A>(Math.ceil(options.replay)) : undefined
if (options.capacity === 1) {
return new BoundedPubSubSingle(replayBuffer)
} else if (nextPow2(options.capacity) === options.capacity) {
return new BoundedPubSubPow2(options.capacity, replayBuffer)
} else {
return new BoundedPubSubArb(options.capacity, replayBuffer)
}
}
/**
* Creates an unbounded atomic PubSub implementation with optional replay buffer.
*
* **When to use**
*
* Use to create the low-level storage layer for a custom `PubSub` whose active
* subscribers may retain an unbounded number of pending messages.
*
* **Gotchas**
*
* Messages published while subscribers are active can be retained without a
* capacity limit until those subscribers take them or unsubscribe.
*
* @see {@link makeAtomicBounded} for a bounded atomic implementation that enforces capacity
* @see {@link make} for wrapping an atomic implementation with a delivery strategy
* @see {@link unbounded} for the high-level effectful constructor for unbounded `PubSub` values
*
* @category constructors
* @since 4.0.0
*/
export const makeAtomicUnbounded = <A>(options?: {
readonly replay?: number | undefined
}): PubSub.Atomic<A> => new UnboundedPubSub(options?.replay ? new ReplayBuffer(options.replay) : undefined)
/**
* Returns the number of elements the queue can hold.
*
* **Example** (Getting PubSub capacity)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(100)
* const cap = PubSub.capacity(pubsub)
* console.log("PubSub capacity:", cap) // 100
*
* const unboundedPubsub = yield* PubSub.unbounded<string>()
* const unboundedCap = PubSub.capacity(unboundedPubsub)
* console.log("Unbounded capacity:", unboundedCap) // Number.MAX_SAFE_INTEGER
* })
* ```
*
* @category getters
* @since 2.0.0
*/
export const capacity = <A>(self: PubSub<A>): number => self.pubsub.capacity
/**
* Returns the current number of messages retained by the `PubSub` for active
* subscribers.
*
* **Details**
*
* If the `PubSub` has been shut down, the returned effect succeeds with `0`.
* The size is not a count of waiting subscribers or suspended publishers.
*
* **Example** (Getting PubSub size)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Initially empty
* const initialSize = yield* PubSub.size(pubsub)
* console.log("Initial size:", initialSize) // 0
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish some messages for the active subscription
* yield* PubSub.publish(pubsub, "msg1")
* yield* PubSub.publish(pubsub, "msg2")
*
* const afterPublish = yield* PubSub.size(pubsub)
* console.log("After publishing:", afterPublish) // 2
*
* yield* PubSub.takeAll(subscription)
* }))
* })
* ```
*
* @category getters
* @since 2.0.0
*/
export const size = <A>(self: PubSub<A>): Effect.Effect<number> => Effect.sync(() => sizeUnsafe(self))
/**
* Returns the current number of messages retained by the `PubSub` for active
* subscribers synchronously.
*
* **When to use**
*
* Use when an immediate `PubSub` size snapshot is needed outside effectful code
* and concurrent changes between the check and later use are acceptable.
*
* **Details**
*
* Returns `0` after shutdown. Because this is an unsafe synchronous snapshot,
* prefer `size` in effectful code.
*
* **Example** (Reading size synchronously)
*
* ```ts
* import { PubSub } from "effect"
*
* // Unsafe synchronous size check
* declare const pubsub: PubSub.PubSub<string>
*
* const size = PubSub.sizeUnsafe(pubsub)
* console.log("Current size:", size)
* ```
*
* @category getters
* @since 4.0.0
*/
export const sizeUnsafe = <A>(self: PubSub<A>): number => {
if (MutableRef.get(self.shutdownFlag)) {
return 0
}
return self.pubsub.size()
}
/**
* Returns `true` when the `PubSub` has reached its configured capacity.
*
* **Details**
*
* For unbounded PubSubs this is normally `false`.
*
* **Example** (Checking whether a PubSub is full)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(2)
*
* // Initially not full
* const initiallyFull = yield* PubSub.isFull(pubsub)
* console.log("Initially full:", initiallyFull) // false
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Fill the PubSub for the active subscription
* yield* PubSub.publish(pubsub, "msg1")
* yield* PubSub.publish(pubsub, "msg2")
*
* const nowFull = yield* PubSub.isFull(pubsub)
* console.log("Now full:", nowFull) // true
*
* yield* PubSub.takeAll(subscription)
* }))
* })
* ```
*
* @category predicates
* @since 2.0.0
*/
export const isFull = <A>(self: PubSub<A>): Effect.Effect<boolean> =>
Effect.map(size(self), (size) => size === self.pubsub.capacity)
/**
* Returns `true` if the `Pubsub` contains zero elements, `false` otherwise.
*
* **Example** (Checking whether a PubSub is empty)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Initially empty
* const initiallyEmpty = yield* PubSub.isEmpty(pubsub)
* console.log("Initially empty:", initiallyEmpty) // true
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish a message for the active subscription
* yield* PubSub.publish(pubsub, "Hello")
*
* const nowEmpty = yield* PubSub.isEmpty(pubsub)
* console.log("Now empty:", nowEmpty) // false
*
* yield* PubSub.take(subscription)
* }))
* })
* ```
*
* @category predicates
* @since 2.0.0
*/
export const isEmpty = <A>(self: PubSub<A>): Effect.Effect<boolean> => Effect.map(size(self), (size) => size === 0)
/**
* Shuts down the `PubSub`, interrupting suspended publishers and subscribers
* and finalizing active subscriptions.
*
* **Details**
*
* After shutdown, `publish` and `publishAll` succeed with `false`,
* `publishUnsafe` returns `false`, and subscription operations such as `take`
* interrupt.
*
* **Example** (Shutting down a PubSub)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(1)
*
* // Shutdown the PubSub
* yield* PubSub.shutdown(pubsub)
*
* const isShutdown = yield* PubSub.isShutdown(pubsub)
* console.log("Is shutdown:", isShutdown) // true
*
* // Publishing after shutdown returns false
* const published = yield* PubSub.publish(pubsub, "msg1")
* console.log("Published after shutdown:", published) // false
* })
* ```
*
* @category lifecycle
* @since 2.0.0
*/
export const shutdown = <A>(self: PubSub<A>): Effect.Effect<void> =>
Effect.uninterruptible(Effect.withFiber((fiber) => {
MutableRef.set(self.shutdownFlag, true)
return Scope.close(self.scope, Exit.interrupt(fiber.id)).pipe(
Effect.andThen(self.strategy.shutdown),
Effect.when(self.shutdownHook.open),
Effect.asVoid
)
}))
/**
* Checks effectfully whether `shutdown` has been called, returning `true`
* after shutdown and `false` otherwise.
*
* **Example** (Checking whether a PubSub is shutdown)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Initially not shutdown
* const initiallyShutdown = yield* PubSub.isShutdown(pubsub)
* console.log("Initially shutdown:", initiallyShutdown) // false
*
* // Shutdown the PubSub
* yield* PubSub.shutdown(pubsub)
*
* const nowShutdown = yield* PubSub.isShutdown(pubsub)
* console.log("Now shutdown:", nowShutdown) // true
* })
* ```
*
* @category predicates
* @since 2.0.0
*/
export const isShutdown = <A>(self: PubSub<A>): Effect.Effect<boolean> => Effect.sync(() => isShutdownUnsafe(self))
/**
* Checks synchronously whether `shutdown` has been called, returning `true`
* after shutdown and `false` otherwise.
*
* **When to use**
*
* Use when an immediate `PubSub` shutdown-state snapshot is needed outside
* effectful code and racing shutdown changes are acceptable.
*
* **Example** (Checking shutdown synchronously)
*
* ```ts
* import { PubSub } from "effect"
*
* declare const pubsub: PubSub.PubSub<string>
*
* // Unsafe synchronous shutdown check
* const isDown = PubSub.isShutdownUnsafe(pubsub)
* if (isDown) {
* console.log("PubSub is shutdown, cannot publish")
* } else {
* console.log("PubSub is active")
* }
* ```
*
* @category predicates
* @since 4.0.0
*/
export const isShutdownUnsafe = <A>(self: PubSub<A>): boolean => self.shutdownFlag.current
/**
* Waits until the queue is shutdown. The `Effect` returned by this method will
* not resume until the queue has been shutdown. If the queue is already
* shutdown, the `Effect` will resume right away.
*
* **Example** (Waiting for shutdown)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Start a fiber that waits for shutdown
* const waiterFiber = yield* Effect.forkChild(
* Effect.gen(function*() {
* yield* PubSub.awaitShutdown(pubsub)
* console.log("PubSub has been shutdown!")
* })
* )
*
* // Do some work...
* yield* Effect.sleep("100 millis")
*
* // Shutdown the PubSub
* yield* PubSub.shutdown(pubsub)
*
* // The waiter will now complete
* yield* Fiber.join(waiterFiber)
* })
* ```
*
* @category lifecycle
* @since 2.0.0
*/
export const awaitShutdown = <A>(self: PubSub<A>): Effect.Effect<void> => self.shutdownHook.await
/**
* Publishes a message to the `PubSub` as an `Effect`, returning whether the
* message was accepted.
*
* **When to use**
*
* Use when you need to publish from effectful code and let the configured
* PubSub strategy handle surplus messages.
*
* **Details**
*
* The effect succeeds with `false` if the `PubSub` is shut down. If the message
* cannot be accepted immediately, the configured strategy decides how surplus
* messages are handled.
*
* **Example** (Publishing a message)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish a message
* const published = yield* PubSub.publish(pubsub, "Hello World")
* console.log("Message published:", published) // true
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* yield* PubSub.publish(pubsub, "Hello")
* const message = yield* PubSub.take(subscription)
* console.log("Received:", message) // "Hello"
* }))
* })
* ```
*
* @see {@link publishUnsafe} for a synchronous non-blocking attempt that does not run effectful surplus handling
*
* @category publishing
* @since 2.0.0
*/
export const publish: {
/**
* Publishes a message to the `PubSub` as an `Effect`, returning whether the
* message was accepted.
*
* **When to use**
*
* Use when you need to publish from effectful code and let the configured
* PubSub strategy handle surplus messages.
*
* **Details**
*
* The effect succeeds with `false` if the `PubSub` is shut down. If the message
* cannot be accepted immediately, the configured strategy decides how surplus
* messages are handled.
*
* **Example** (Publishing a message)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish a message
* const published = yield* PubSub.publish(pubsub, "Hello World")
* console.log("Message published:", published) // true
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* yield* PubSub.publish(pubsub, "Hello")
* const message = yield* PubSub.take(subscription)
* console.log("Received:", message) // "Hello"
* }))
* })
* ```
*
* @see {@link publishUnsafe} for a synchronous non-blocking attempt that does not run effectful surplus handling
*
* @category publishing
* @since 2.0.0
*/
<A>(value: A): (self: PubSub<A>) => Effect.Effect<boolean>
/**
* Publishes a message to the `PubSub` as an `Effect`, returning whether the
* message was accepted.
*
* **When to use**
*
* Use when you need to publish from effectful code and let the configured
* PubSub strategy handle surplus messages.
*
* **Details**
*
* The effect succeeds with `false` if the `PubSub` is shut down. If the message
* cannot be accepted immediately, the configured strategy decides how surplus
* messages are handled.
*
* **Example** (Publishing a message)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish a message
* const published = yield* PubSub.publish(pubsub, "Hello World")
* console.log("Message published:", published) // true
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* yield* PubSub.publish(pubsub, "Hello")
* const message = yield* PubSub.take(subscription)
* console.log("Received:", message) // "Hello"
* }))
* })
* ```
*
* @see {@link publishUnsafe} for a synchronous non-blocking attempt that does not run effectful surplus handling
*
* @category publishing
* @since 2.0.0
*/
<A>(self: PubSub<A>, value: A): Effect.Effect<boolean>
} = dual(2, <A>(self: PubSub<A>, value: A): Effect.Effect<boolean> =>
Effect.suspend(() => {
if (self.shutdownFlag.current) {
return Effect.succeed(false)
}
if (self.pubsub.publish(value)) {
self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers)
return Effect.succeed(true)
}
return self.strategy.handleSurplus(
self.pubsub,
self.subscribers,
[value],
self.shutdownFlag
)
}))
/**
* Attempts to publish a message synchronously without applying the PubSub
* strategy's effectful surplus handling.
*
* **When to use**
*
* Use when you need a non-blocking synchronous publish attempt where `false`
* is an acceptable result when the message cannot be accepted immediately.
*
* **Details**
*
* Returns `false` if the `PubSub` is shut down or the message cannot be
* accepted immediately, for example when a bounded PubSub is full. Prefer
* `publish` when backpressure or sliding behavior should be honored.
*
* **Example** (Publishing without suspending)
*
* ```ts
* import { PubSub } from "effect"
*
* declare const pubsub: PubSub.PubSub<string>
*
* // Unsafe synchronous publish (non-blocking)
* const published = PubSub.publishUnsafe(pubsub, "Hello")
* if (published) {
* console.log("Message published successfully")
* } else {
* console.log("Message dropped (PubSub full or shutdown)")
* }
*
* // Useful for scenarios where you don't want to suspend
* const messages = ["msg1", "msg2", "msg3"]
* const publishedCount =
* messages.filter((msg) => PubSub.publishUnsafe(pubsub, msg)).length
* console.log(`Published ${publishedCount} out of ${messages.length} messages`)
* ```
*
* @see {@link publish} for effectful publishing that honors the configured surplus strategy
*
* @category publishing
* @since 4.0.0
*/
export const publishUnsafe: {
/**
* Attempts to publish a message synchronously without applying the PubSub
* strategy's effectful surplus handling.
*
* **When to use**
*
* Use when you need a non-blocking synchronous publish attempt where `false`
* is an acceptable result when the message cannot be accepted immediately.
*
* **Details**
*
* Returns `false` if the `PubSub` is shut down or the message cannot be
* accepted immediately, for example when a bounded PubSub is full. Prefer
* `publish` when backpressure or sliding behavior should be honored.
*
* **Example** (Publishing without suspending)
*
* ```ts
* import { PubSub } from "effect"
*
* declare const pubsub: PubSub.PubSub<string>
*
* // Unsafe synchronous publish (non-blocking)
* const published = PubSub.publishUnsafe(pubsub, "Hello")
* if (published) {
* console.log("Message published successfully")
* } else {
* console.log("Message dropped (PubSub full or shutdown)")
* }
*
* // Useful for scenarios where you don't want to suspend
* const messages = ["msg1", "msg2", "msg3"]
* const publishedCount =
* messages.filter((msg) => PubSub.publishUnsafe(pubsub, msg)).length
* console.log(`Published ${publishedCount} out of ${messages.length} messages`)
* ```
*
* @see {@link publish} for effectful publishing that honors the configured surplus strategy
*
* @category publishing
* @since 4.0.0
*/
<A>(value: A): (self: PubSub<A>) => boolean
/**
* Attempts to publish a message synchronously without applying the PubSub
* strategy's effectful surplus handling.
*
* **When to use**
*
* Use when you need a non-blocking synchronous publish attempt where `false`
* is an acceptable result when the message cannot be accepted immediately.
*
* **Details**
*
* Returns `false` if the `PubSub` is shut down or the message cannot be
* accepted immediately, for example when a bounded PubSub is full. Prefer
* `publish` when backpressure or sliding behavior should be honored.
*
* **Example** (Publishing without suspending)
*
* ```ts
* import { PubSub } from "effect"
*
* declare const pubsub: PubSub.PubSub<string>
*
* // Unsafe synchronous publish (non-blocking)
* const published = PubSub.publishUnsafe(pubsub, "Hello")
* if (published) {
* console.log("Message published successfully")
* } else {
* console.log("Message dropped (PubSub full or shutdown)")
* }
*
* // Useful for scenarios where you don't want to suspend
* const messages = ["msg1", "msg2", "msg3"]
* const publishedCount =
* messages.filter((msg) => PubSub.publishUnsafe(pubsub, msg)).length
* console.log(`Published ${publishedCount} out of ${messages.length} messages`)
* ```
*
* @see {@link publish} for effectful publishing that honors the configured surplus strategy
*
* @category publishing
* @since 4.0.0
*/
<A>(self: PubSub<A>, value: A): boolean
} = dual(2, <A>(self: PubSub<A>, value: A): boolean => {
if (self.shutdownFlag.current) return false
if (self.pubsub.publish(value)) {
self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers)
return true
}
return false
})
/**
* Publishes all of the specified messages to the `PubSub`, returning whether they
* were published to the `PubSub`.
*
* **Example** (Publishing multiple messages)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish multiple messages at once
* const messages = ["Hello", "World", "from", "Effect"]
* const allPublished = yield* PubSub.publishAll(pubsub, messages)
* console.log("All messages published:", allPublished) // true
*
* // With a smaller capacity and an active subscription
* const smallPubsub = yield* PubSub.bounded<string>(2)
* const manyMessages = ["msg1", "msg2", "msg3", "msg4"]
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(smallPubsub)
*
* // Will suspend until space becomes available for all messages
* const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, manyMessages))
*
* const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2)
* console.log("First batch:", firstBatch) // ["msg1", "msg2"]
*
* const result = yield* Fiber.join(fiber)
* console.log("All messages eventually published:", result) // true
*
* const secondBatch = yield* PubSub.takeAll(subscription)
* console.log("Second batch:", secondBatch) // ["msg3", "msg4"]
* }))
* })
* ```
*
* @category publishing
* @since 2.0.0
*/
export const publishAll: {
/**
* Publishes all of the specified messages to the `PubSub`, returning whether they
* were published to the `PubSub`.
*
* **Example** (Publishing multiple messages)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish multiple messages at once
* const messages = ["Hello", "World", "from", "Effect"]
* const allPublished = yield* PubSub.publishAll(pubsub, messages)
* console.log("All messages published:", allPublished) // true
*
* // With a smaller capacity and an active subscription
* const smallPubsub = yield* PubSub.bounded<string>(2)
* const manyMessages = ["msg1", "msg2", "msg3", "msg4"]
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(smallPubsub)
*
* // Will suspend until space becomes available for all messages
* const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, manyMessages))
*
* const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2)
* console.log("First batch:", firstBatch) // ["msg1", "msg2"]
*
* const result = yield* Fiber.join(fiber)
* console.log("All messages eventually published:", result) // true
*
* const secondBatch = yield* PubSub.takeAll(subscription)
* console.log("Second batch:", secondBatch) // ["msg3", "msg4"]
* }))
* })
* ```
*
* @category publishing
* @since 2.0.0
*/
<A>(elements: Iterable<A>): (self: PubSub<A>) => Effect.Effect<boolean>
/**
* Publishes all of the specified messages to the `PubSub`, returning whether they
* were published to the `PubSub`.
*
* **Example** (Publishing multiple messages)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Publish multiple messages at once
* const messages = ["Hello", "World", "from", "Effect"]
* const allPublished = yield* PubSub.publishAll(pubsub, messages)
* console.log("All messages published:", allPublished) // true
*
* // With a smaller capacity and an active subscription
* const smallPubsub = yield* PubSub.bounded<string>(2)
* const manyMessages = ["msg1", "msg2", "msg3", "msg4"]
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(smallPubsub)
*
* // Will suspend until space becomes available for all messages
* const fiber = yield* Effect.forkChild(PubSub.publishAll(smallPubsub, manyMessages))
*
* const firstBatch = yield* PubSub.takeBetween(subscription, 2, 2)
* console.log("First batch:", firstBatch) // ["msg1", "msg2"]
*
* const result = yield* Fiber.join(fiber)
* console.log("All messages eventually published:", result) // true
*
* const secondBatch = yield* PubSub.takeAll(subscription)
* console.log("Second batch:", secondBatch) // ["msg3", "msg4"]
* }))
* })
* ```
*
* @category publishing
* @since 2.0.0
*/
<A>(self: PubSub<A>, elements: Iterable<A>): Effect.Effect<boolean>
} = dual(2, <A>(self: PubSub<A>, elements: Iterable<A>): Effect.Effect<boolean> =>
Effect.suspend(() => {
if (self.shutdownFlag.current) {
return Effect.succeed(false)
}
const surplus = self.pubsub.publishAll(elements)
self.strategy.completeSubscribersUnsafe(self.pubsub, self.subscribers)
if (surplus.length === 0) {
return Effect.succeed(true)
}
return self.strategy.handleSurplus(
self.pubsub,
self.subscribers,
surplus,
self.shutdownFlag
)
}))
/**
* Subscribes to receive messages from the `PubSub`. The resulting subscription can
* be evaluated multiple times within the scope to take a message from the `PubSub`
* each time.
*
* **Example** (Subscribing to messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* // Subscribe within a scope for automatic cleanup
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish some messages
* yield* PubSub.publish(pubsub, "Hello")
* yield* PubSub.publish(pubsub, "World")
*
* // Take messages one by one
* const msg1 = yield* PubSub.take(subscription)
* const msg2 = yield* PubSub.take(subscription)
* console.log(msg1, msg2) // "Hello", "World"
*
* // Subscription is automatically cleaned up when scope exits
* }))
*
* yield* Effect.scoped(Effect.gen(function*() {
* const sub1 = yield* PubSub.subscribe(pubsub)
* const sub2 = yield* PubSub.subscribe(pubsub)
*
* // Multiple subscribers can receive the same messages
* yield* PubSub.publish(pubsub, "Broadcast")
*
* const [msg1, msg2] = yield* Effect.all([
* PubSub.take(sub1),
* PubSub.take(sub2)
* ])
* console.log("Both received:", msg1, msg2) // "Broadcast", "Broadcast"
* }))
* })
* ```
*
* @category subscriptions
* @since 2.0.0
*/
export const subscribe = <A>(self: PubSub<A>): Effect.Effect<Subscription<A>, never, Scope.Scope> =>
Effect.uninterruptible(
Effect.contextWith((services) => {
const localScope = Context.get(services, Scope.Scope)
const scope = Scope.forkUnsafe(self.scope)
const subscription = makeSubscriptionUnsafe(self.pubsub, self.subscribers, self.strategy)
return Scope.addFinalizer(scope, unsubscribe(subscription)).pipe(
Effect.andThen(Scope.addFinalizerExit(localScope, (exit) => Scope.close(scope, exit))),
Effect.as(subscription)
)
})
)
const unsubscribe = <A>(self: Subscription<A>): Effect.Effect<void> =>
Effect.uninterruptible(
Effect.withFiber<void>((state) => {
MutableRef.set(self.shutdownFlag, true)
return Effect.forEach(
MutableList.takeAll(self.pollers),
(d) => Deferred.interruptWith(d, state.id),
{ discard: true, concurrency: "unbounded" }
).pipe(
Effect.tap(() =>
Effect.sync(() => {
self.subscribers.delete(self.subscription)
self.subscription.unsubscribe()
self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers)
})
),
Effect.when(self.shutdownHook.open),
Effect.asVoid
)
})
)
/**
* Takes a single message from the subscription. If no messages are available,
* this will suspend until a message becomes available.
*
* **Example** (Taking a message)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Start a fiber to take a message (will suspend)
* const takeFiber = yield* Effect.forkChild(
* PubSub.take(subscription)
* )
*
* // Publish a message
* yield* PubSub.publish(pubsub, "Hello")
*
* // The take will now complete
* const message = yield* Fiber.join(takeFiber)
* console.log("Received:", message) // "Hello"
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
export const take = <A>(self: Subscription<A>): Effect.Effect<A> =>
Effect.suspend(() => {
if (self.shutdownFlag.current) {
return Effect.interrupt
}
if (self.replayWindow.remaining > 0) {
const message = self.replayWindow.take()!
return Effect.succeed(message)
}
const message = self.pollers.length === 0
? self.subscription.poll()
: MutableList.Empty
if (message === MutableList.Empty) {
return pollForItem(self)
} else {
self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers)
return Effect.succeed(message)
}
})
/**
* Takes all available messages from the subscription, suspending if no items
* are available.
*
* **Example** (Taking all available messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish multiple messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Take all available messages at once
* const allMessages = yield* PubSub.takeAll(subscription)
* console.log("All messages:", allMessages) // ["msg1", "msg2", "msg3"]
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
export const takeAll = <A>(self: Subscription<A>): Effect.Effect<Arr.NonEmptyArray<A>> =>
Effect.suspend(function loop(value?: [A]): Effect.Effect<Arr.NonEmptyArray<A>> {
if (self.shutdownFlag.current) {
return Effect.interrupt
}
let as = self.pollers.length === 0
? self.subscription.pollUpTo(Number.POSITIVE_INFINITY)
: []
if (value) {
as = value.concat(as)
}
self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers)
if (self.replayWindow.remaining > 0) {
return Effect.succeed(self.replayWindow.takeAll().concat(as) as Arr.NonEmptyArray<A>)
} else if (!Arr.isArrayNonEmpty(as)) {
return Effect.flatMap(pollForItem(self), (item) => loop([item]))
}
return Effect.succeed(as)
})
const pollForItem = <A>(self: Subscription<A>) => {
const deferred = Deferred.makeUnsafe<A>()
let set = self.subscribers.get(self.subscription)
if (!set) {
set = new Set()
self.subscribers.set(self.subscription, set)
}
set.add(self.pollers)
MutableList.append(self.pollers, deferred)
self.strategy.completePollersUnsafe(
self.pubsub,
self.subscribers,
self.subscription,
self.pollers
)
return Effect.onInterrupt(
Deferred.await(deferred),
() => {
MutableList.remove(self.pollers, deferred)
return Effect.void
}
)
}
/**
* Takes up to the specified number of messages from the subscription without suspending.
*
* **Example** (Taking up to a maximum number of messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish multiple messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"])
*
* // Take up to 3 messages
* const upTo3 = yield* PubSub.takeUpTo(subscription, 3)
* console.log("Up to 3:", upTo3) // ["msg1", "msg2", "msg3"]
*
* // Take up to 5 more (only 2 remaining)
* const upTo5 = yield* PubSub.takeUpTo(subscription, 5)
* console.log("Up to 5:", upTo5) // ["msg4", "msg5"]
*
* // No more messages available
* const noMore = yield* PubSub.takeUpTo(subscription, 10)
* console.log("No more:", noMore) // []
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
export const takeUpTo: {
/**
* Takes up to the specified number of messages from the subscription without suspending.
*
* **Example** (Taking up to a maximum number of messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish multiple messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"])
*
* // Take up to 3 messages
* const upTo3 = yield* PubSub.takeUpTo(subscription, 3)
* console.log("Up to 3:", upTo3) // ["msg1", "msg2", "msg3"]
*
* // Take up to 5 more (only 2 remaining)
* const upTo5 = yield* PubSub.takeUpTo(subscription, 5)
* console.log("Up to 5:", upTo5) // ["msg4", "msg5"]
*
* // No more messages available
* const noMore = yield* PubSub.takeUpTo(subscription, 10)
* console.log("No more:", noMore) // []
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
(max: number): <A>(self: Subscription<A>) => Effect.Effect<Array<A>>
/**
* Takes up to the specified number of messages from the subscription without suspending.
*
* **Example** (Taking up to a maximum number of messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish multiple messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3", "msg4", "msg5"])
*
* // Take up to 3 messages
* const upTo3 = yield* PubSub.takeUpTo(subscription, 3)
* console.log("Up to 3:", upTo3) // ["msg1", "msg2", "msg3"]
*
* // Take up to 5 more (only 2 remaining)
* const upTo5 = yield* PubSub.takeUpTo(subscription, 5)
* console.log("Up to 5:", upTo5) // ["msg4", "msg5"]
*
* // No more messages available
* const noMore = yield* PubSub.takeUpTo(subscription, 10)
* console.log("No more:", noMore) // []
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
<A>(self: Subscription<A>, max: number): Effect.Effect<Array<A>>
} = dual(2, <A>(self: Subscription<A>, max: number): Effect.Effect<Array<A>> =>
Effect.suspend(() => {
if (self.shutdownFlag.current) return Effect.interrupt
let replay: Array<A> | undefined = undefined
if (self.replayWindow.remaining >= max) {
return Effect.succeed(self.replayWindow.takeN(max))
} else if (self.replayWindow.remaining > 0) {
replay = self.replayWindow.takeAll()
max = max - replay.length
}
const as = self.pollers.length === 0
? self.subscription.pollUpTo(max)
: []
self.strategy.onPubSubEmptySpaceUnsafe(self.pubsub, self.subscribers)
return replay ? Effect.succeed(replay.concat(as)) : Effect.succeed(as)
}))
/**
* Takes between the specified minimum and maximum number of messages from the subscription.
* Will suspend if the minimum number is not immediately available.
*
* **Example** (Taking between a minimum and maximum)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Start taking between 2 and 5 messages (will suspend)
* const takeFiber = yield* Effect.forkChild(
* PubSub.takeBetween(subscription, 2, 5)
* )
*
* // Publish 3 messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Now the take will complete with 3 messages
* const messages = yield* Fiber.join(takeFiber)
* console.log("Between 2-5:", messages) // ["msg1", "msg2", "msg3"]
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
export const takeBetween: {
/**
* Takes between the specified minimum and maximum number of messages from the subscription.
* Will suspend if the minimum number is not immediately available.
*
* **Example** (Taking between a minimum and maximum)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Start taking between 2 and 5 messages (will suspend)
* const takeFiber = yield* Effect.forkChild(
* PubSub.takeBetween(subscription, 2, 5)
* )
*
* // Publish 3 messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Now the take will complete with 3 messages
* const messages = yield* Fiber.join(takeFiber)
* console.log("Between 2-5:", messages) // ["msg1", "msg2", "msg3"]
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
(min: number, max: number): <A>(self: Subscription<A>) => Effect.Effect<Array<A>>
/**
* Takes between the specified minimum and maximum number of messages from the subscription.
* Will suspend if the minimum number is not immediately available.
*
* **Example** (Taking between a minimum and maximum)
*
* ```ts
* import { Effect, Fiber, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Start taking between 2 and 5 messages (will suspend)
* const takeFiber = yield* Effect.forkChild(
* PubSub.takeBetween(subscription, 2, 5)
* )
*
* // Publish 3 messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Now the take will complete with 3 messages
* const messages = yield* Fiber.join(takeFiber)
* console.log("Between 2-5:", messages) // ["msg1", "msg2", "msg3"]
* }))
* })
* ```
*
* @category subscriptions
* @since 4.0.0
*/
<A>(self: Subscription<A>, min: number, max: number): Effect.Effect<Array<A>>
} = dual(
3,
<A>(self: Subscription<A>, min: number, max: number): Effect.Effect<Array<A>> =>
Effect.suspend(() => takeRemainderLoop(self, min, max, []))
)
const takeRemainderLoop = <A>(
self: Subscription<A>,
min: number,
max: number,
acc: Array<A>
): Effect.Effect<Array<A>> => {
if (max < min) {
return Effect.succeed(acc)
}
return Effect.flatMap(takeUpTo(self, max), (bs) => {
acc.push(...bs)
const remaining = min - bs.length
if (remaining === 1) {
return Effect.map(take(self), (b) => {
acc.push(b)
return acc
})
}
if (remaining > 1) {
return Effect.flatMap(take(self), (b) => {
acc.push(b)
return takeRemainderLoop(
self,
remaining - 1,
max - bs.length - 1,
acc
)
})
}
return Effect.succeed(acc)
})
}
/**
* Returns the number of messages currently available in the subscription as an
* `Effect`.
*
* **When to use**
*
* Use when checking a subscription from effectful code and shutdown should
* interrupt the effect.
*
* **Details**
*
* The count includes replay-buffered messages. If the subscription has been
* shut down, the effect interrupts.
*
* **Example** (Checking remaining messages)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* const pubsub = yield* PubSub.bounded<string>(10)
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish some messages
* yield* PubSub.publishAll(pubsub, ["msg1", "msg2", "msg3"])
*
* // Check how many messages are available
* const count = yield* PubSub.remaining(subscription)
* console.log("Messages available:", count) // 3
*
* // Take one message
* yield* PubSub.take(subscription)
*
* const remaining = yield* PubSub.remaining(subscription)
* console.log("Messages remaining:", remaining) // 2
* }))
* })
* ```
*
* @see {@link remainingUnsafe} for a synchronous check that reports shutdown as `Option.none()`
*
* @category getters
* @since 4.0.0
*/
export const remaining = <A>(self: Subscription<A>): Effect.Effect<number> =>
Effect.suspend(() =>
self.shutdownFlag.current
? Effect.interrupt
: Effect.succeed(self.subscription.size() + self.replayWindow.remaining)
)
/**
* Synchronously returns the number of messages currently available in the
* subscription, or `Option.none()` when it is shut down.
*
* **When to use**
*
* Use when you need synchronous polling outside a managed workflow and want
* shutdown observed as data instead of interruption.
*
* **Example** (Checking remaining messages synchronously)
*
* ```ts
* import { PubSub } from "effect"
*
* declare const subscription: PubSub.Subscription<string>
*
* // Unsafe synchronous check for remaining messages
* const remainingOption = PubSub.remainingUnsafe(subscription)
* if (remainingOption._tag === "Some") {
* console.log("Messages available:", remainingOption.value)
* } else {
* console.log("Subscription is shutdown")
* }
*
* // Useful for polling or batching scenarios
* if (remainingOption._tag === "Some" && remainingOption.value > 10) {
* // Process messages in batch
* }
* ```
*
* @see {@link remaining} for the effectful variant that interrupts on shutdown
*
* @category getters
* @since 4.0.0
*/
export const remainingUnsafe = <A>(self: Subscription<A>): Option.Option<number> => {
if (self.shutdownFlag.current) {
return Option.none()
}
return Option.some(self.subscription.size() + self.replayWindow.remaining)
}
// -----------------------------------------------------------------------------
// internal
// -----------------------------------------------------------------------------
const AbsentValue = Symbol.for("effect/PubSub/AbsentValue")
type AbsentValue = typeof AbsentValue
const addSubscribers = <A>(
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
) => {
if (!subscribers.has(subscription)) {
subscribers.set(subscription, new Set())
}
const set = subscribers.get(subscription)!
set.add(pollers)
}
const removeSubscribers = <A>(
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
) => {
if (!subscribers.has(subscription)) {
return
}
const set = subscribers.get(subscription)!
set.delete(pollers)
if (set.size === 0) {
subscribers.delete(subscription)
}
}
const makeSubscriptionUnsafe = <A>(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
strategy: PubSub.Strategy<A>
): Subscription<A> =>
new SubscriptionImpl(
pubsub,
subscribers,
pubsub.subscribe(),
MutableList.make<Deferred.Deferred<A>>(),
Latch.makeUnsafe(false),
MutableRef.make(false),
strategy,
pubsub.replayWindow()
)
class BoundedPubSubArb<in out A> implements PubSub.Atomic<A> {
array: Array<A>
publisherIndex = 0
subscribers: Array<number>
subscriberCount = 0
subscribersIndex = 0
readonly capacity: number
readonly replayBuffer: ReplayBuffer<A> | undefined
constructor(capacity: number, replayBuffer: ReplayBuffer<A> | undefined) {
this.capacity = capacity
this.replayBuffer = replayBuffer
this.array = Array.from({ length: capacity })
this.subscribers = Array.from({ length: capacity })
}
replayWindow(): PubSub.ReplayWindow<A> {
return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow
}
isEmpty(): boolean {
return this.publisherIndex === this.subscribersIndex
}
isFull(): boolean {
return this.publisherIndex === this.subscribersIndex + this.capacity
}
size(): number {
return this.publisherIndex - this.subscribersIndex
}
publish(value: A): boolean {
if (this.isFull()) {
return false
}
if (this.subscriberCount !== 0) {
const index = this.publisherIndex % this.capacity
this.array[index] = value
this.subscribers[index] = this.subscriberCount
this.publisherIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.offer(value)
}
return true
}
publishAll(elements: Iterable<A>): Array<A> {
if (this.subscriberCount === 0) {
if (this.replayBuffer) {
this.replayBuffer.offerAll(elements)
}
return []
}
const chunk = Arr.fromIterable(elements)
const n = chunk.length
const size = this.publisherIndex - this.subscribersIndex
const available = this.capacity - size
const forPubSub = Math.min(n, available)
if (forPubSub === 0) {
return chunk
}
let iteratorIndex = 0
const publishAllIndex = this.publisherIndex + forPubSub
while (this.publisherIndex !== publishAllIndex) {
const a = chunk[iteratorIndex++]
const index = this.publisherIndex % this.capacity
this.array[index] = a
this.subscribers[index] = this.subscriberCount
this.publisherIndex += 1
if (this.replayBuffer) {
this.replayBuffer.offer(a)
}
}
return chunk.slice(iteratorIndex)
}
slide(): void {
if (this.subscribersIndex !== this.publisherIndex) {
const index = this.subscribersIndex % this.capacity
this.array[index] = AbsentValue as unknown as A
this.subscribers[index] = 0
this.subscribersIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.slide()
}
}
subscribe(): PubSub.BackingSubscription<A> {
this.subscriberCount += 1
return new BoundedPubSubArbSubscription(this, this.publisherIndex, false)
}
}
class BoundedPubSubArbSubscription<in out A> implements PubSub.BackingSubscription<A> {
private self: BoundedPubSubArb<A>
private subscriberIndex: number
private unsubscribed: boolean
constructor(
self: BoundedPubSubArb<A>,
subscriberIndex: number,
unsubscribed: boolean
) {
this.self = self
this.subscriberIndex = subscriberIndex
this.unsubscribed = unsubscribed
}
isEmpty(): boolean {
return (
this.unsubscribed ||
this.self.publisherIndex === this.subscriberIndex ||
this.self.publisherIndex === this.self.subscribersIndex
)
}
size() {
if (this.unsubscribed) {
return 0
}
return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex)
}
poll(): A | MutableList.Empty {
if (this.unsubscribed) {
return MutableList.Empty
}
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
if (this.subscriberIndex !== this.self.publisherIndex) {
const index = this.subscriberIndex % this.self.capacity
const elem = this.self.array[index]!
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
this.subscriberIndex += 1
return elem
}
return MutableList.Empty
}
pollUpTo(n: number): Array<A> {
if (this.unsubscribed) {
return []
}
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
const size = this.self.publisherIndex - this.subscriberIndex
const toPoll = Math.min(n, size)
if (toPoll <= 0) {
return []
}
const builder: Array<A> = []
const pollUpToIndex = this.subscriberIndex + toPoll
while (this.subscriberIndex !== pollUpToIndex) {
const index = this.subscriberIndex % this.self.capacity
const a = this.self.array[index] as A
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
builder.push(a)
this.subscriberIndex += 1
}
return builder
}
unsubscribe(): void {
if (!this.unsubscribed) {
this.unsubscribed = true
this.self.subscriberCount -= 1
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
while (this.subscriberIndex !== this.self.publisherIndex) {
const index = this.subscriberIndex % this.self.capacity
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
this.subscriberIndex += 1
}
}
}
}
class BoundedPubSubPow2<in out A> implements PubSub.Atomic<A> {
array: Array<A>
mask: number
publisherIndex = 0
subscribers: Array<number>
subscriberCount = 0
subscribersIndex = 0
readonly capacity: number
readonly replayBuffer: ReplayBuffer<A> | undefined
constructor(capacity: number, replayBuffer: ReplayBuffer<A> | undefined) {
this.capacity = capacity
this.replayBuffer = replayBuffer
this.array = Array.from({ length: capacity })
this.mask = capacity - 1
this.subscribers = Array.from({ length: capacity })
}
replayWindow(): PubSub.ReplayWindow<A> {
return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow
}
isEmpty(): boolean {
return this.publisherIndex === this.subscribersIndex
}
isFull(): boolean {
return this.publisherIndex === this.subscribersIndex + this.capacity
}
size(): number {
return this.publisherIndex - this.subscribersIndex
}
publish(value: A): boolean {
if (this.isFull()) {
return false
}
if (this.subscriberCount !== 0) {
const index = this.publisherIndex & this.mask
this.array[index] = value
this.subscribers[index] = this.subscriberCount
this.publisherIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.offer(value)
}
return true
}
publishAll(elements: Iterable<A>): Array<A> {
if (this.subscriberCount === 0) {
if (this.replayBuffer) {
this.replayBuffer.offerAll(elements)
}
return []
}
const chunk = Arr.fromIterable(elements)
const n = chunk.length
const size = this.publisherIndex - this.subscribersIndex
const available = this.capacity - size
const forPubSub = Math.min(n, available)
if (forPubSub === 0) {
return chunk
}
let iteratorIndex = 0
const publishAllIndex = this.publisherIndex + forPubSub
while (this.publisherIndex !== publishAllIndex) {
const elem = chunk[iteratorIndex++]
const index = this.publisherIndex & this.mask
this.array[index] = elem
this.subscribers[index] = this.subscriberCount
this.publisherIndex += 1
if (this.replayBuffer) {
this.replayBuffer.offer(elem)
}
}
return chunk.slice(iteratorIndex)
}
slide(): void {
if (this.subscribersIndex !== this.publisherIndex) {
const index = this.subscribersIndex & this.mask
this.array[index] = AbsentValue as unknown as A
this.subscribers[index] = 0
this.subscribersIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.slide()
}
}
subscribe(): PubSub.BackingSubscription<A> {
this.subscriberCount += 1
return new BoundedPubSubPow2Subscription(this, this.publisherIndex, false)
}
}
class BoundedPubSubPow2Subscription<in out A> implements PubSub.BackingSubscription<A> {
private self: BoundedPubSubPow2<A>
private subscriberIndex: number
private unsubscribed: boolean
constructor(
self: BoundedPubSubPow2<A>,
subscriberIndex: number,
unsubscribed: boolean
) {
this.self = self
this.subscriberIndex = subscriberIndex
this.unsubscribed = unsubscribed
}
isEmpty(): boolean {
return (
this.unsubscribed ||
this.self.publisherIndex === this.subscriberIndex ||
this.self.publisherIndex === this.self.subscribersIndex
)
}
size() {
if (this.unsubscribed) {
return 0
}
return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex)
}
poll(): A | MutableList.Empty {
if (this.unsubscribed) {
return MutableList.Empty
}
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
if (this.subscriberIndex !== this.self.publisherIndex) {
const index = this.subscriberIndex & this.self.mask
const elem = this.self.array[index]!
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
this.subscriberIndex += 1
return elem
}
return MutableList.Empty
}
pollUpTo(n: number): Array<A> {
if (this.unsubscribed) {
return []
}
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
const size = this.self.publisherIndex - this.subscriberIndex
const toPoll = Math.min(n, size)
if (toPoll <= 0) {
return []
}
const builder: Array<A> = []
const pollUpToIndex = this.subscriberIndex + toPoll
while (this.subscriberIndex !== pollUpToIndex) {
const index = this.subscriberIndex & this.self.mask
const elem = this.self.array[index] as A
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
builder.push(elem)
this.subscriberIndex += 1
}
return builder
}
unsubscribe(): void {
if (!this.unsubscribed) {
this.unsubscribed = true
this.self.subscriberCount -= 1
this.subscriberIndex = Math.max(this.subscriberIndex, this.self.subscribersIndex)
while (this.subscriberIndex !== this.self.publisherIndex) {
const index = this.subscriberIndex & this.self.mask
this.self.subscribers[index] -= 1
if (this.self.subscribers[index] === 0) {
this.self.array[index] = AbsentValue as unknown as A
this.self.subscribersIndex += 1
}
this.subscriberIndex += 1
}
}
}
}
class BoundedPubSubSingle<in out A> implements PubSub.Atomic<A> {
publisherIndex = 0
subscriberCount = 0
subscribers = 0
value: A = AbsentValue as unknown as A
readonly capacity = 1
readonly replayBuffer: ReplayBuffer<A> | undefined
constructor(replayBuffer: ReplayBuffer<A> | undefined) {
this.replayBuffer = replayBuffer
}
replayWindow(): PubSub.ReplayWindow<A> {
return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow
}
pipe() {
return pipeArguments(this, arguments)
}
isEmpty(): boolean {
return this.subscribers === 0
}
isFull(): boolean {
return !this.isEmpty()
}
size(): number {
return this.isEmpty() ? 0 : 1
}
publish(value: A): boolean {
if (this.isFull()) {
return false
}
if (this.subscriberCount !== 0) {
this.value = value
this.subscribers = this.subscriberCount
this.publisherIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.offer(value)
}
return true
}
publishAll(elements: Iterable<A>): Array<A> {
if (this.subscriberCount === 0) {
if (this.replayBuffer) {
this.replayBuffer.offerAll(elements)
}
return []
}
const chunk = Arr.fromIterable(elements)
if (chunk.length === 0) {
return chunk
}
if (this.publish(chunk[0])) {
return chunk.slice(1)
} else {
return chunk
}
}
slide(): void {
if (this.isFull()) {
this.subscribers = 0
this.value = AbsentValue as unknown as A
}
if (this.replayBuffer) {
this.replayBuffer.slide()
}
}
subscribe(): PubSub.BackingSubscription<A> {
this.subscriberCount += 1
return new BoundedPubSubSingleSubscription(this, this.publisherIndex, false)
}
}
class BoundedPubSubSingleSubscription<in out A> implements PubSub.BackingSubscription<A> {
private self: BoundedPubSubSingle<A>
private subscriberIndex: number
private unsubscribed: boolean
constructor(
self: BoundedPubSubSingle<A>,
subscriberIndex: number,
unsubscribed: boolean
) {
this.self = self
this.subscriberIndex = subscriberIndex
this.unsubscribed = unsubscribed
}
isEmpty(): boolean {
return (
this.unsubscribed ||
this.self.subscribers === 0 ||
this.subscriberIndex === this.self.publisherIndex
)
}
size() {
return this.isEmpty() ? 0 : 1
}
poll(): A | MutableList.Empty {
if (this.isEmpty()) {
return MutableList.Empty
}
const elem = this.self.value
this.self.subscribers -= 1
if (this.self.subscribers === 0) {
this.self.value = AbsentValue as unknown as A
}
this.subscriberIndex += 1
return elem
}
pollUpTo(n: number): Array<A> {
if (this.isEmpty() || n < 1) {
return []
}
const a = this.self.value
this.self.subscribers -= 1
if (this.self.subscribers === 0) {
this.self.value = AbsentValue as unknown as A
}
this.subscriberIndex += 1
return [a]
}
unsubscribe(): void {
if (!this.unsubscribed) {
this.unsubscribed = true
this.self.subscriberCount -= 1
if (this.subscriberIndex !== this.self.publisherIndex) {
this.self.subscribers -= 1
if (this.self.subscribers === 0) {
this.self.value = AbsentValue as unknown as A
}
}
}
}
}
interface Node<out A> {
value: A | AbsentValue
subscribers: number
next: Node<A> | null
}
class UnboundedPubSub<in out A> implements PubSub.Atomic<A> {
publisherHead: Node<A> = {
value: AbsentValue,
subscribers: 0,
next: null
}
publisherTail = this.publisherHead
publisherIndex = 0
subscribersIndex = 0
readonly capacity = Number.MAX_SAFE_INTEGER
readonly replayBuffer: ReplayBuffer<A> | undefined
constructor(replayBuffer: ReplayBuffer<A> | undefined) {
this.replayBuffer = replayBuffer
}
replayWindow(): PubSub.ReplayWindow<A> {
return this.replayBuffer ? new ReplayWindowImpl(this.replayBuffer) : emptyReplayWindow
}
isEmpty(): boolean {
return this.publisherHead === this.publisherTail
}
isFull(): boolean {
return false
}
size(): number {
return this.publisherIndex - this.subscribersIndex
}
publish(value: A): boolean {
const subscribers = this.publisherTail.subscribers
if (subscribers !== 0) {
this.publisherTail.next = {
value,
subscribers,
next: null
}
this.publisherTail = this.publisherTail.next
this.publisherIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.offer(value)
}
return true
}
publishAll(elements: Iterable<A>): Array<A> {
if (this.publisherTail.subscribers !== 0) {
for (const a of elements) {
this.publish(a)
}
} else if (this.replayBuffer) {
this.replayBuffer.offerAll(elements)
}
return []
}
slide(): void {
if (this.publisherHead !== this.publisherTail) {
this.publisherHead = this.publisherHead.next!
this.publisherHead.value = AbsentValue
this.subscribersIndex += 1
}
if (this.replayBuffer) {
this.replayBuffer.slide()
}
}
subscribe(): PubSub.BackingSubscription<A> {
this.publisherTail.subscribers += 1
return new UnboundedPubSubSubscription(
this,
this.publisherTail,
this.publisherIndex,
false
)
}
}
class UnboundedPubSubSubscription<in out A> implements PubSub.BackingSubscription<A> {
private self: UnboundedPubSub<A>
private subscriberHead: Node<A>
private subscriberIndex: number
private unsubscribed: boolean
constructor(
self: UnboundedPubSub<A>,
subscriberHead: Node<A>,
subscriberIndex: number,
unsubscribed: boolean
) {
this.self = self
this.subscriberHead = subscriberHead
this.subscriberIndex = subscriberIndex
this.unsubscribed = unsubscribed
}
isEmpty(): boolean {
if (this.unsubscribed) {
return true
}
let empty = true
let loop = true
while (loop) {
if (this.subscriberHead === this.self.publisherTail) {
loop = false
} else {
if (this.subscriberHead.next!.value !== AbsentValue) {
empty = false
loop = false
} else {
this.subscriberHead = this.subscriberHead.next!
this.subscriberIndex += 1
}
}
}
return empty
}
size() {
if (this.unsubscribed) {
return 0
}
return this.self.publisherIndex - Math.max(this.subscriberIndex, this.self.subscribersIndex)
}
poll(): A | MutableList.Empty {
if (this.unsubscribed) {
return MutableList.Empty
}
let loop = true
let polled: A | MutableList.Empty = MutableList.Empty
while (loop) {
if (this.subscriberHead === this.self.publisherTail) {
loop = false
} else {
const elem = this.subscriberHead.next!.value
if (elem !== AbsentValue) {
polled = elem
this.subscriberHead.subscribers -= 1
if (this.subscriberHead.subscribers === 0) {
this.self.publisherHead = this.self.publisherHead.next!
this.self.publisherHead.value = AbsentValue
this.self.subscribersIndex += 1
}
loop = false
}
this.subscriberHead = this.subscriberHead.next!
this.subscriberIndex += 1
}
}
return polled
}
pollUpTo(n: number): Array<A> {
const builder: Array<A> = []
let i = 0
while (i !== n) {
const a = this.poll()
if (a === MutableList.Empty) {
i = n
} else {
builder.push(a)
i += 1
}
}
return builder
}
unsubscribe(): void {
if (!this.unsubscribed) {
this.unsubscribed = true
this.self.publisherTail.subscribers -= 1
while (this.subscriberHead !== this.self.publisherTail) {
if (this.subscriberHead.next!.value !== AbsentValue) {
this.subscriberHead.subscribers -= 1
if (this.subscriberHead.subscribers === 0) {
this.self.publisherHead = this.self.publisherHead.next!
this.self.publisherHead.value = AbsentValue
this.self.subscribersIndex += 1
}
}
this.subscriberHead = this.subscriberHead.next!
}
}
}
}
class SubscriptionImpl<in out A> implements Subscription<A> {
readonly [SubscriptionTypeId] = {
_A: identity
}
readonly pubsub: PubSub.Atomic<A>
readonly subscribers: PubSub.Subscribers<A>
readonly subscription: PubSub.BackingSubscription<A>
readonly pollers: MutableList.MutableList<Deferred.Deferred<A>>
readonly shutdownHook: Latch.Latch
readonly shutdownFlag: MutableRef.MutableRef<boolean>
readonly strategy: PubSub.Strategy<A>
readonly replayWindow: PubSub.ReplayWindow<A>
constructor(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>,
shutdownHook: Latch.Latch,
shutdownFlag: MutableRef.MutableRef<boolean>,
strategy: PubSub.Strategy<A>,
replayWindow: PubSub.ReplayWindow<A>
) {
this.pubsub = pubsub
this.subscribers = subscribers
this.subscription = subscription
this.pollers = pollers
this.shutdownHook = shutdownHook
this.shutdownFlag = shutdownFlag
this.strategy = strategy
this.replayWindow = replayWindow
}
pipe() {
return pipeArguments(this, arguments)
}
}
class PubSubImpl<in out A> implements PubSub<A> {
readonly [TypeId] = {
_A: identity
}
readonly pubsub: PubSub.Atomic<A>
readonly subscribers: PubSub.Subscribers<A>
readonly scope: Scope.Closeable
readonly shutdownHook: Latch.Latch
readonly shutdownFlag: MutableRef.MutableRef<boolean>
readonly strategy: PubSub.Strategy<A>
constructor(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
scope: Scope.Closeable,
shutdownHook: Latch.Latch,
shutdownFlag: MutableRef.MutableRef<boolean>,
strategy: PubSub.Strategy<A>
) {
this.pubsub = pubsub
this.subscribers = subscribers
this.scope = scope
this.shutdownHook = shutdownHook
this.shutdownFlag = shutdownFlag
this.strategy = strategy
}
pipe() {
return pipeArguments(this, arguments)
}
}
const makePubSubUnsafe = <A>(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
scope: Scope.Closeable,
shutdownHook: Latch.Latch,
shutdownFlag: MutableRef.MutableRef<boolean>,
strategy: PubSub.Strategy<A>
): PubSub<A> => new PubSubImpl(pubsub, subscribers, scope, shutdownHook, shutdownFlag, strategy)
const ensureCapacity = (capacity: number): void => {
if (capacity <= 0) {
throw new Error(`Cannot construct PubSub with capacity of ${capacity}`)
}
}
// -----------------------------------------------------------------------------
// PubSub.Strategy
// -----------------------------------------------------------------------------
/**
* Represents the back-pressure strategy for bounded `PubSub` values.
*
* **When to use**
*
* Use to preserve every message for current subscribers when a bounded custom
* `PubSub` should make publishers wait for capacity instead of dropping or
* evicting messages.
*
* **Details**
*
* Publishers wait when the `PubSub` is at capacity, so all current subscribers
* can receive every published message.
*
* **Gotchas**
*
* A slow subscriber can slow down publishers and other subscribers.
*
* @see {@link bounded} for creating bounded PubSubs with back pressure by default
* @see {@link DroppingStrategy} for dropping new messages when capacity is full
* @see {@link SlidingStrategy} for evicting old messages when capacity is full
*
* @category models
* @since 4.0.0
*/
export class BackPressureStrategy<in out A> implements PubSub.Strategy<A> {
publishers: MutableList.MutableList<
readonly [A, Deferred.Deferred<boolean>, boolean]
> = MutableList.make()
get shutdown(): Effect.Effect<void> {
return Effect.withFiber((fiber) =>
Effect.forEach(
MutableList.takeAll(this.publishers),
([_, deferred, last]) => last ? Deferred.interruptWith(deferred, fiber.id) : Effect.void,
{ concurrency: "unbounded", discard: true }
)
)
}
handleSurplus(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
elements: Iterable<A>,
isShutdown: MutableRef.MutableRef<boolean>
): Effect.Effect<boolean> {
return Effect.suspend(() => {
const deferred = Deferred.makeUnsafe<boolean>()
this.offerUnsafe(elements, deferred)
this.onPubSubEmptySpaceUnsafe(pubsub, subscribers)
this.completeSubscribersUnsafe(pubsub, subscribers)
return (MutableRef.get(isShutdown) ? Effect.interrupt : Deferred.await(deferred)).pipe(
Effect.onInterrupt(() => {
this.removeUnsafe(deferred)
return Effect.void
})
)
})
}
onPubSubEmptySpaceUnsafe(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>
): void {
let keepPolling = true
while (keepPolling && !pubsub.isFull()) {
const publisher = MutableList.take(this.publishers)
if (publisher === MutableList.Empty) {
keepPolling = false
} else {
const [value, deferred] = publisher
const published = pubsub.publish(value)
if (published && publisher[2]) {
Deferred.doneUnsafe(deferred, Exit.succeed(true))
} else if (!published) {
MutableList.prepend(this.publishers, publisher)
}
this.completeSubscribersUnsafe(pubsub, subscribers)
}
}
}
completePollersUnsafe(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
): void {
return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers)
}
completeSubscribersUnsafe(pubsub: PubSub.Atomic<A>, subscribers: PubSub.Subscribers<A>): void {
return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers)
}
private offerUnsafe(elements: Iterable<A>, deferred: Deferred.Deferred<boolean>): void {
const iterator = elements[Symbol.iterator]()
let next: IteratorResult<A> = iterator.next()
if (!next.done) {
// oxlint-disable-next-line no-constant-condition
while (1) {
const value = next.value
next = iterator.next()
if (next.done) {
MutableList.append(this.publishers, [value, deferred, true])
break
}
MutableList.append(this.publishers, [value, deferred, false])
}
}
}
removeUnsafe(deferred: Deferred.Deferred<boolean>): void {
MutableList.filter(this.publishers, ([_, d]) => d !== deferred)
}
}
/**
* Represents the dropping strategy for bounded `PubSub` values.
*
* **When to use**
*
* Use to keep publishers fast by dropping new messages when the `PubSub` is at
* capacity.
*
* **Details**
*
* A publish that arrives while the `PubSub` is full is dropped instead of
* waiting for capacity.
*
* **Gotchas**
*
* Subscribers may miss messages published while they are subscribed.
*
* **Example** (Using a dropping strategy)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create PubSub with dropping strategy
* const pubsub = yield* PubSub.dropping<string>(2)
*
* // Or explicitly create with dropping strategy
* const customPubsub = yield* PubSub.make<string>({
* atomicPubSub: () => PubSub.makeAtomicBounded(2),
* strategy: () => new PubSub.DroppingStrategy()
* })
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Fill the PubSub
* const pub1 = yield* PubSub.publish(pubsub, "msg1") // true
* const pub2 = yield* PubSub.publish(pubsub, "msg2") // true
* const pub3 = yield* PubSub.publish(pubsub, "msg3") // false (dropped)
*
* console.log("Publication results:", [pub1, pub2, pub3]) // [true, true, false]
*
* // Subscribers will only see the first two messages
* const messages = yield* PubSub.takeAll(subscription)
* console.log("Received messages:", messages) // ["msg1", "msg2"]
* }))
* })
* ```
*
* @category models
* @since 4.0.0
*/
export class DroppingStrategy<in out A> implements PubSub.Strategy<A> {
get shutdown(): Effect.Effect<void> {
return Effect.void
}
handleSurplus(
_pubsub: PubSub.Atomic<A>,
_subscribers: PubSub.Subscribers<A>,
_elements: Iterable<A>,
_isShutdown: MutableRef.MutableRef<boolean>
): Effect.Effect<boolean> {
return Effect.succeed(false)
}
onPubSubEmptySpaceUnsafe(
_pubsub: PubSub.Atomic<A>,
_subscribers: PubSub.Subscribers<A>
): void {
//
}
completePollersUnsafe(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
): void {
return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers)
}
completeSubscribersUnsafe(pubsub: PubSub.Atomic<A>, subscribers: PubSub.Subscribers<A>): void {
return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers)
}
}
/**
* Represents the sliding strategy for bounded `PubSub` values.
*
* **When to use**
*
* Use to keep the most recent messages when the `PubSub` is at capacity.
*
* **Details**
*
* New messages are accepted by evicting older messages from the bounded
* `PubSub`.
*
* **Gotchas**
*
* Slow subscribers may miss older messages that are evicted before they are
* consumed.
*
* **Example** (Using a sliding strategy)
*
* ```ts
* import { Effect, PubSub } from "effect"
*
* const program = Effect.gen(function*() {
* // Create PubSub with sliding strategy
* const pubsub = yield* PubSub.sliding<string>(2)
*
* // Or explicitly create with sliding strategy
* const customPubsub = yield* PubSub.make<string>({
* atomicPubSub: () => PubSub.makeAtomicBounded(2),
* strategy: () => new PubSub.SlidingStrategy()
* })
*
* yield* Effect.scoped(Effect.gen(function*() {
* const subscription = yield* PubSub.subscribe(pubsub)
*
* // Publish messages that exceed capacity
* yield* PubSub.publish(pubsub, "msg1") // stored
* yield* PubSub.publish(pubsub, "msg2") // stored
* yield* PubSub.publish(pubsub, "msg3") // "msg1" evicted, "msg3" stored
* yield* PubSub.publish(pubsub, "msg4") // "msg2" evicted, "msg4" stored
*
* // Subscribers will see the most recent messages
* const messages = yield* PubSub.takeAll(subscription)
* console.log("Recent messages:", messages) // ["msg3", "msg4"]
* }))
* })
* ```
*
* @category models
* @since 4.0.0
*/
export class SlidingStrategy<in out A> implements PubSub.Strategy<A> {
get shutdown(): Effect.Effect<void> {
return Effect.void
}
handleSurplus(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
elements: Iterable<A>,
_isShutdown: MutableRef.MutableRef<boolean>
): Effect.Effect<boolean> {
return Effect.sync(() => {
this.slidingPublishUnsafe(pubsub, elements)
this.completeSubscribersUnsafe(pubsub, subscribers)
return true
})
}
onPubSubEmptySpaceUnsafe(
_pubsub: PubSub.Atomic<A>,
_subscribers: PubSub.Subscribers<A>
): void {
//
}
completePollersUnsafe(
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
): void {
return strategyCompletePollersUnsafe(this, pubsub, subscribers, subscription, pollers)
}
completeSubscribersUnsafe(pubsub: PubSub.Atomic<A>, subscribers: PubSub.Subscribers<A>): void {
return strategyCompleteSubscribersUnsafe(this, pubsub, subscribers)
}
slidingPublishUnsafe(pubsub: PubSub.Atomic<A>, elements: Iterable<A>): void {
const it = elements[Symbol.iterator]()
let next = it.next()
if (!next.done && pubsub.capacity > 0) {
let a = next.value
let loop = true
while (loop) {
pubsub.slide()
const pub = pubsub.publish(a)
if (pub && (next = it.next()) && !next.done) {
a = next.value
} else if (pub) {
loop = false
}
}
}
}
}
const strategyCompletePollersUnsafe = <A>(
strategy: PubSub.Strategy<A>,
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>,
subscription: PubSub.BackingSubscription<A>,
pollers: MutableList.MutableList<Deferred.Deferred<A>>
): void => {
let keepPolling = true
while (keepPolling && !subscription.isEmpty()) {
const poller = MutableList.take(pollers)
if (poller === MutableList.Empty) {
removeSubscribers(subscribers, subscription, pollers)
if (pollers.length === 0) {
keepPolling = false
} else {
addSubscribers(subscribers, subscription, pollers)
}
} else {
const pollResult = subscription.poll()
if (pollResult === MutableList.Empty) {
MutableList.prepend(pollers, poller)
} else {
Deferred.doneUnsafe(poller, Exit.succeed(pollResult))
strategy.onPubSubEmptySpaceUnsafe(pubsub, subscribers)
}
}
}
}
const strategyCompleteSubscribersUnsafe = <A>(
strategy: PubSub.Strategy<A>,
pubsub: PubSub.Atomic<A>,
subscribers: PubSub.Subscribers<A>
): void => {
for (
const [subscription, pollersSet] of subscribers
) {
for (const pollers of pollersSet) {
strategy.completePollersUnsafe(pubsub, subscribers, subscription, pollers)
}
}
}
interface ReplayNode<A> {
value: A | AbsentValue
next: ReplayNode<A> | null
}
class ReplayBuffer<A> {
readonly capacity: number
head: ReplayNode<A> = { value: AbsentValue, next: null }
tail: ReplayNode<A> = this.head
size = 0
index = 0
constructor(capacity: number) {
this.capacity = capacity
}
slide() {
this.index++
}
offer(a: A): void {
this.tail.value = a
this.tail.next = {
value: AbsentValue,
next: null
}
this.tail = this.tail.next
if (this.size === this.capacity) {
this.head = this.head.next!
} else {
this.size += 1
}
}
offerAll(as: Iterable<A>): void {
for (const a of as) {
this.offer(a)
}
}
}
class ReplayWindowImpl<A> implements PubSub.ReplayWindow<A> {
head: ReplayNode<A>
index: number
remaining: number
readonly buffer: ReplayBuffer<A>
constructor(buffer: ReplayBuffer<A>) {
this.buffer = buffer
this.index = buffer.index
this.remaining = buffer.size
this.head = buffer.head
}
fastForward() {
while (this.index < this.buffer.index) {
this.head = this.head.next!
this.index++
}
}
take(): A | undefined {
if (this.remaining === 0) {
return undefined
} else if (this.index < this.buffer.index) {
this.fastForward()
}
this.remaining--
const value = this.head.value
this.head = this.head.next!
return value as A
}
takeN(n: number): Array<A> {
if (this.remaining === 0) {
return []
} else if (this.index < this.buffer.index) {
this.fastForward()
}
const len = Math.min(n, this.remaining)
const items = new Array(len)
for (let i = 0; i < len; i++) {
const value = this.head.value as A
this.head = this.head.next!
items[i] = value
}
this.remaining -= len
return items
}
takeAll(): Array<A> {
return this.takeN(this.remaining)
}
}
const emptyReplayWindow: PubSub.ReplayWindow<never> = {
remaining: 0,
take: () => undefined,
takeN: () => [],
takeAll: () => []
}

Xet Storage Details

Size:
94.4 kB
·
Xet hash:
a1509b2371f08b810e34a32e5fe38d7a51216faf1deaac6c5673e559fcd84228

Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.