已发布 上游基线 bf46254 原文 ↗ 在 GitHub 编辑

SubscriptionRef

了解如何在 Effect 中使用 SubscriptionRef 管理共享状态,让多个观察者能够订阅并高效地响应并发环境中的状态变化。

SubscriptionRef<A>SynchronizedRef 的一种特化形式。它让我们可以订阅当前值以及对该值所做的任何更改,并接收相应的更新。

interface SubscriptionRef<A> {
  readonly value: A
}

/**
 * A stream containing the current value of the `Ref` as well as all changes
 * to that value.
 */
declare const changes: <A>(self: SubscriptionRef<A>) => Stream<A>

你可以对 SubscriptionRef 执行所有标准操作,例如用 getsetmodify 与当前值交互。

SubscriptionRef 的关键特性是它的 changes 流。这个流让你能够观察到订阅那一刻的当前值,并接收之后所有的变化。每次运行该流时,它都会发出当前值,并跟踪后续的更新。

要创建一个 SubscriptionRef,你可以使用 SubscriptionRef.make 构造函数,并指定初始值:

示例(创建一个 SubscriptionRef

import { SubscriptionRef, Effect } from "effect"

const ref = SubscriptionRef.make(0)

await Effect.runPromise(Effect.map(ref, (r) => r.value)) // => 0

当多个观察者需要对变化做出反应时,SubscriptionRef 尤其适合用于对共享状态建模。例如在函数式响应式编程中,SubscriptionRef 可以表示应用状态的一部分,而各种观察者(比如 UI 组件)会随状态变化而更新。

示例(使用 SubscriptionRef 的服务器-客户端模型)

在下面的示例中,一个“服务器”持续更新共享值,而多个“客户端”观察这些变化:

import { SubscriptionRef, Effect, Fiber } from "effect"

// Server function that increments a shared value forever
const server = (ref: SubscriptionRef.SubscriptionRef<number>) =>
  SubscriptionRef.update(ref, (n) => n + 1).pipe(Effect.forever)

// Run the server briefly, then interrupt it, to confirm it does increment
await Effect.runPromise(
  Effect.gen(function* () {
    const ref = yield* SubscriptionRef.make(0)
    const fiber = yield* Effect.forkChild(server(ref))
    yield* Effect.sleep("50 millis")
    yield* Fiber.interrupt(fiber)
    return (yield* SubscriptionRef.get(ref)) > 0
  }),
) // => true

server 函数操作的是一个普通的 Ref,并持续更新该值。它不需要直接了解 SubscriptionRef

接下来,我们定义一个 client,它订阅变化并收集指定数量的值:

import { SubscriptionRef, Effect, Stream, Random } from "effect"

// Server function that increments a shared value forever
const server = (ref: SubscriptionRef.SubscriptionRef<number>) =>
  SubscriptionRef.update(ref, (n) => n + 1).pipe(Effect.forever)

// Client function that observes the stream of changes
const client = (changes: Stream.Stream<number>) =>
  Effect.gen(function* () {
    const n = yield* Random.nextIntBetween(1, 10)
    const chunk = yield* Stream.runCollect(Stream.take(changes, n))
    return chunk
  })

// Exercise client with a deterministic (seeded) source stream
const testStream = Stream.iterate(1, (n) => n + 1)
await Effect.runPromise(client(testStream).pipe(Random.withSeed("seed"))) // => [1, 2]

同样地,client 函数只处理值的 Stream,并不关心这些值的来源。

为了把所有部分串起来,我们启动服务器,并行启动多个客户端实例,然后在我们完成后关闭服务器。我们还会在这个过程中创建 SubscriptionRef

import { Effect, Stream, Random, SubscriptionRef, Fiber } from "effect"

// Server function that increments a shared value forever
const server = (ref: SubscriptionRef.SubscriptionRef<number>) =>
  SubscriptionRef.update(ref, (n) => n + 1).pipe(Effect.forever)

// Client function that observes the stream of changes
const client = (changes: Stream.Stream<number>) =>
  Effect.gen(function* () {
    const n = yield* Random.nextIntBetween(1, 10)
    const chunk = yield* Stream.runCollect(Stream.take(changes, n))
    return chunk
  })

const program = Effect.gen(function* () {
  // Create a SubscriptionRef with an initial value of 0
  const ref = yield* SubscriptionRef.make(0)

  // Fork the server to run concurrently
  const serverFiber = yield* Effect.forkChild(server(ref))

  // Create 5 clients that subscribe to the changes stream
  const clients = new Array(5)
    .fill(null)
    .map(() => client(SubscriptionRef.changes(ref)))

  // Run all clients in concurrently and collect their results
  const chunks = yield* Effect.all(clients, { concurrency: "unbounded" })

  // Interrupt the server when clients are done
  yield* Fiber.interrupt(serverFiber)

  // Output the results collected by each client
  for (const chunk of chunks) {
    console.log(chunk)
  }
})

Effect.runPromise(program)
/*
Example Output:
[ 4, 5, 6, 7, 8, 9 ]
[ 4 ]
[ 4, 5, 6, 7, 8, 9 ]
[ 4, 5 ]
[ 4, 5, 6, 7, 8, 9 ]
*/

// The chunk contents and their interleaving are non-deterministic, but each
// of the 5 clients always contributes exactly one chunk
const chunkCount = await Effect.runPromise(
  Effect.gen(function* () {
    const ref = yield* SubscriptionRef.make(0)
    const serverFiber = yield* Effect.forkChild(server(ref))
    const clients = new Array(5)
      .fill(null)
      .map(() => client(SubscriptionRef.changes(ref)))
    const chunks = yield* Effect.all(clients, { concurrency: "unbounded" })
    yield* Fiber.interrupt(serverFiber)
    return chunks.length
  }),
)
chunkCount // => 5

这套设置确保每个客户端在启动时都能观察到当前值,并接收该值之后的所有变化。

由于这些变化以流的形式表示,你可以使用熟悉的流操作符轻松构建更复杂的程序。你可以对这些流进行转换、过滤,或将其与其他流合并,从而实现更精细的行为。