event.ts 5.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157
  1. export * as EventV2 from "./event"
  2. import { Context, Effect, Layer, Option, PubSub, Schema, Stream } from "effect"
  3. import { Location } from "./location"
  4. import { withStatics } from "./schema"
  5. import { Identifier } from "./util/identifier"
  6. export const ID = Schema.String.pipe(
  7. Schema.brand("Event.ID"),
  8. withStatics((schema) => ({ create: () => schema.make("evt_" + Identifier.ascending()) })),
  9. )
  10. export type ID = typeof ID.Type
  11. export type Definition<Type extends string = string, DataSchema extends Schema.Top = Schema.Top> = {
  12. readonly type: Type
  13. readonly version?: number
  14. readonly aggregate?: string
  15. readonly data: DataSchema
  16. }
  17. export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
  18. export type Payload<D extends Definition = Definition> = {
  19. readonly id: ID
  20. readonly type: D["type"]
  21. readonly data: Data<D>
  22. readonly version?: number
  23. readonly location?: Location.Ref
  24. readonly metadata?: Record<string, unknown>
  25. }
  26. export type Sync = (event: Payload) => Effect.Effect<void>
  27. export const registry = new Map<string, Definition>()
  28. export function define<const Type extends string, Fields extends Schema.Struct.Fields>(input: {
  29. readonly type: Type
  30. readonly version?: number
  31. readonly aggregate?: string
  32. readonly schema: Fields
  33. }): Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> & Definition<Type, Schema.Struct<Fields>> {
  34. const Data = Schema.Struct(input.schema)
  35. const Payload = Schema.Struct({
  36. id: ID,
  37. metadata: Schema.optional(Schema.Record(Schema.String, Schema.Unknown)),
  38. type: Schema.Literal(input.type),
  39. version: Schema.optional(Schema.Number),
  40. location: Schema.optional(Location.Ref),
  41. data: Data,
  42. }).annotate({ identifier: input.type })
  43. const definition = Object.assign(Payload, {
  44. type: input.type,
  45. ...(input.version === undefined ? {} : { version: input.version }),
  46. ...(input.aggregate === undefined ? {} : { aggregate: input.aggregate }),
  47. data: Data,
  48. })
  49. registry.set(input.type, definition)
  50. return definition as Schema.Schema<Payload<Definition<Type, Schema.Struct<Fields>>>> &
  51. Definition<Type, Schema.Struct<Fields>>
  52. }
  53. export function definitions() {
  54. return registry.values().toArray()
  55. }
  56. export interface PublishOptions {
  57. readonly id?: ID
  58. readonly metadata?: Record<string, unknown>
  59. }
  60. export type Unsubscribe = Effect.Effect<void>
  61. export interface Interface {
  62. readonly publish: <D extends Definition>(
  63. definition: D,
  64. data: Data<D>,
  65. options?: PublishOptions,
  66. ) => Effect.Effect<Payload<D>>
  67. readonly publishEvent: <D extends Definition>(event: Payload<D>) => Effect.Effect<Payload<D>>
  68. readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
  69. readonly all: () => Stream.Stream<Payload>
  70. readonly sync: (handler: Sync) => Effect.Effect<Unsubscribe>
  71. }
  72. export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
  73. export const layer = Layer.effect(
  74. Service,
  75. Effect.gen(function* () {
  76. const all = yield* PubSub.unbounded<Payload>()
  77. const typed = new Map<string, PubSub.PubSub<Payload>>()
  78. const syncHandlers = new Array<Sync>()
  79. const getOrCreate = (definition: Definition) =>
  80. Effect.gen(function* () {
  81. const existing = typed.get(definition.type)
  82. if (existing) return existing
  83. const pubsub = yield* PubSub.unbounded<Payload>()
  84. typed.set(definition.type, pubsub)
  85. return pubsub
  86. })
  87. yield* Effect.addFinalizer(() =>
  88. Effect.gen(function* () {
  89. yield* PubSub.shutdown(all)
  90. yield* Effect.forEach(typed.values(), PubSub.shutdown, { discard: true })
  91. }),
  92. )
  93. function publishEvent<D extends Definition>(event: Payload<D>) {
  94. return Effect.gen(function* () {
  95. for (const sync of syncHandlers) {
  96. yield* sync(event as Payload)
  97. }
  98. const pubsub = typed.get(event.type)
  99. if (pubsub) yield* PubSub.publish(pubsub, event as Payload)
  100. yield* PubSub.publish(all, event as Payload)
  101. return event
  102. })
  103. }
  104. function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
  105. return Effect.gen(function* () {
  106. const location = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
  107. const event = {
  108. id: options?.id ?? ID.create(),
  109. ...(options?.metadata ? { metadata: options.metadata } : {}),
  110. type: definition.type,
  111. ...(definition.version === undefined ? {} : { version: definition.version }),
  112. ...(location ? { location } : {}),
  113. data,
  114. } as Payload<D>
  115. return yield* publishEvent(event)
  116. })
  117. }
  118. const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
  119. Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
  120. Stream.map((event) => event as Payload<D>),
  121. )
  122. const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(all)
  123. const sync = (handler: Sync): Effect.Effect<Unsubscribe> =>
  124. Effect.sync(() => {
  125. syncHandlers.push(handler)
  126. return Effect.sync(() => {
  127. const index = syncHandlers.indexOf(handler)
  128. if (index >= 0) syncHandlers.splice(index, 1)
  129. })
  130. })
  131. return Service.of({ publish, publishEvent, subscribe, all: streamAll, sync })
  132. }),
  133. )
  134. export const defaultLayer = layer