refactor(core): unify live event subscriptions

This commit is contained in:
Kit Langton 2026-07-06 13:40:19 -04:00
commit 142ce54e72
4 changed files with 15 additions and 19 deletions

View file

@ -118,6 +118,11 @@ export type SubscribePayload<D extends readonly Definition[]> = D[number] extend
: never : never
export interface Subscribe { export interface Subscribe {
/**
* Volatile live channel: every event published from now on, nothing before or
* across a disconnect. Consumers that need reliability combine it with `log`.
*/
(): Stream.Stream<Payload>
<D extends Definition>(definition: D): Stream.Stream<Payload<D>> <D extends Definition>(definition: D): Stream.Stream<Payload<D>>
<const D extends readonly [Definition, ...Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>> <const D extends readonly [Definition, ...Definition[]]>(definitions: D): Stream.Stream<SubscribePayload<D>>
} }
@ -131,12 +136,6 @@ export interface Interface {
options?: PublishOptions, options?: PublishOptions,
) => Effect.Effect<Payload<D>> ) => Effect.Effect<Payload<D>>
readonly subscribe: Subscribe readonly subscribe: Subscribe
/**
* Volatile live channel: every event published from now on, nothing before,
* nothing across a disconnect. The only channel that carries non-durable
* events; consumers that need reliability combine `changes` with `log`.
*/
readonly live: () => Stream.Stream<Payload>
/** /**
* Durable, ordered, gap-free per-aggregate log read. `follow: false` * Durable, ordered, gap-free per-aggregate log read. `follow: false`
* completes at the end of the log; `follow: true` replays then transitions * completes at the end of the log; `follow: true` replays then transitions
@ -150,7 +149,7 @@ export interface Interface {
}) => Stream.Stream<LogItem> }) => Stream.Stream<LogItem>
/** Latest committed seq per aggregate. Aggregates without events are absent. */ /** Latest committed seq per aggregate. Aggregates without events are absent. */
readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>> readonly sequences: (aggregateIDs: ReadonlyArray<string>) => Effect.Effect<ReadonlyMap<string, Seq>>
/** @deprecated Use `all()` and consume the returned stream. */ /** @deprecated Use `subscribe()` and consume the returned stream. */
readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe> readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void> readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
readonly replay: ( readonly replay: (
@ -575,17 +574,16 @@ export const layerWith = (options?: LayerOptions) =>
), ),
) )
const subscribeOne = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> => function subscribe(): Stream.Stream<Payload>
local(Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub))))).pipe(
Stream.map((event) => event as Payload<D>),
)
function subscribe<D extends Definition>(definition: D): Stream.Stream<Payload<D>> function subscribe<D extends Definition>(definition: D): Stream.Stream<Payload<D>>
function subscribe<const D extends readonly [Definition, ...Definition[]]>( function subscribe<const D extends readonly [Definition, ...Definition[]]>(
definitions: D, definitions: D,
): Stream.Stream<SubscribePayload<D>> ): Stream.Stream<SubscribePayload<D>>
function subscribe(input: Definition | readonly Definition[]): Stream.Stream<Payload> { function subscribe(input?: Definition | readonly Definition[]): Stream.Stream<Payload> {
if (isDefinition(input)) return subscribeOne(input) if (input === undefined) return streamLive()
if (isDefinition(input)) {
return local(Stream.unwrap(getOrCreate(input).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))))
}
const types = new Set(input.map((definition) => definition.type)) const types = new Set(input.map((definition) => definition.type))
return streamLive().pipe(Stream.filter((event) => types.has(event.type))) return streamLive().pipe(Stream.filter((event) => types.has(event.type)))
} }
@ -737,7 +735,6 @@ export const layerWith = (options?: LayerOptions) =>
return Service.of({ return Service.of({
publish, publish,
subscribe, subscribe,
live: streamLive,
log, log,
sequences, sequences,
listen, listen,

View file

@ -159,7 +159,7 @@ export const make = Effect.fn("PluginHost.make")(function* (plugin: PluginV2.Int
}), }),
}, },
event: { event: {
subscribe: () => events.live().pipe(Stream.filter(EventManifest.isServer)), subscribe: () => events.subscribe().pipe(Stream.filter(EventManifest.isServer)),
}, },
integration: { integration: {
list: () => response(integration.list()), list: () => response(integration.list()),

View file

@ -177,7 +177,7 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const typed = yield* events.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
const wildcard = yield* events.live().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped) const wildcard = yield* events.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
yield* Effect.yieldNow yield* Effect.yieldNow
const event = yield* events.publish(Message, { text: "hello" }) const event = yield* events.publish(Message, { text: "hello" })
@ -258,7 +258,7 @@ describe("EventV2", () => {
Effect.gen(function* () { Effect.gen(function* () {
const events = yield* EventV2.Service const events = yield* EventV2.Service
const received = new Array<string>() const received = new Array<string>()
const fiber = yield* events.live().pipe( const fiber = yield* events.subscribe().pipe(
Stream.take(1), Stream.take(1),
Stream.runForEach(() => Effect.sync(() => received.push("stream"))), Stream.runForEach(() => Effect.sync(() => received.push("stream"))),
Effect.forkScoped, Effect.forkScoped,

View file

@ -27,7 +27,6 @@ const capture = () => {
return event return event
}), }),
subscribe: () => Stream.empty, subscribe: () => Stream.empty,
live: () => Stream.empty,
log: () => Stream.empty, log: () => Stream.empty,
sequences: () => Effect.succeed(new Map()), sequences: () => Effect.succeed(new Map()),
listen: () => Effect.succeed(Effect.void), listen: () => Effect.succeed(Effect.void),