event.ts 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198
  1. export * as Event from "./event.js"
  2. import { Schema, SchemaTransformation } from "effect"
  3. import { optional } from "./schema.js"
  4. import { ascending } from "./identifier.js"
  5. import { Location } from "./location.js"
  6. import { DateTimeUtcFromMillis, statics } from "./schema.js"
  7. export const ID = Schema.String.check(Schema.isStartsWith("evt_")).pipe(
  8. Schema.brand("Event.ID"),
  9. statics((schema) => ({ create: () => schema.make("evt_" + ascending()) })),
  10. )
  11. export type ID = typeof ID.Type
  12. /**
  13. * Position in one aggregate's durable log. Values originate from the durable
  14. * event envelope and synced markers;
  15. * `after` cursors accept only values that came from those sources.
  16. */
  17. export const Seq = Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)).pipe(Schema.brand("Event.Seq"))
  18. export type Seq = typeof Seq.Type
  19. /** Durable schema version of one event type, from the event definition that committed it. */
  20. export const Version = Schema.Int.check(Schema.isGreaterThanOrEqualTo(1)).pipe(Schema.brand("Event.Version"))
  21. export type Version = typeof Version.Type
  22. const DurableEnvelope = Schema.Struct({ aggregateID: Schema.String, seq: Seq, version: Version })
  23. export type DurableEnvelope = typeof DurableEnvelope.Type
  24. export type DurableDefinition<
  25. Type extends string = string,
  26. DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
  27. > = Schema.Top & {
  28. readonly type: Type
  29. readonly durability: "durable"
  30. readonly durable: {
  31. readonly version: number
  32. readonly aggregate: string
  33. }
  34. readonly data: DataSchema
  35. }
  36. export type EphemeralDefinition<
  37. Type extends string = string,
  38. DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
  39. > = Schema.Top & {
  40. readonly type: Type
  41. readonly durability: "ephemeral"
  42. readonly durable?: never
  43. readonly data: DataSchema
  44. }
  45. export type Definition<
  46. Type extends string = string,
  47. DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
  48. > = DurableDefinition<Type, DataSchema> | EphemeralDefinition<Type, DataSchema>
  49. export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
  50. type PayloadBase<D extends Definition> = {
  51. readonly id: ID
  52. readonly type: D["type"]
  53. readonly created: typeof DateTimeUtcFromMillis.Type
  54. readonly data: Data<D>
  55. readonly location?: Location.Ref
  56. readonly metadata?: Record<string, unknown>
  57. }
  58. export type Payload<D extends Definition = Definition> = D extends DurableDefinition
  59. ? PayloadBase<D> & { readonly durable: DurableEnvelope }
  60. : PayloadBase<D> & { readonly durable?: never }
  61. type Input<Type extends string, Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>> = {
  62. readonly type: Type
  63. readonly identifier?: string
  64. readonly durable?: {
  65. readonly version: number
  66. readonly aggregate: string
  67. }
  68. readonly schema: Fields
  69. }
  70. export function durable<
  71. const Type extends string,
  72. const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
  73. >(input: Input<Type, Fields> & { readonly durable: NonNullable<Input<Type, Fields>["durable"]> }) {
  74. const data = Schema.Struct(input.schema)
  75. const durable = Schema.Struct({
  76. aggregateID: DurableEnvelope.fields.aggregateID,
  77. seq: DurableEnvelope.fields.seq,
  78. version: Schema.Literal(input.durable.version).pipe(
  79. Schema.decodeTo(
  80. Schema.toType(Version),
  81. SchemaTransformation.transform({
  82. decode: () => Version.make(input.durable.version),
  83. encode: () => input.durable.version,
  84. }),
  85. ),
  86. ),
  87. })
  88. return Schema.Struct({
  89. id: ID,
  90. created: DateTimeUtcFromMillis,
  91. metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
  92. type: Schema.Literal(input.type),
  93. durable,
  94. location: optional(Location.Ref),
  95. data,
  96. })
  97. .annotate({ identifier: input.identifier ?? input.type })
  98. .pipe(
  99. statics(() => ({
  100. type: input.type,
  101. durability: "durable" as const,
  102. durable: input.durable,
  103. data,
  104. })),
  105. ) satisfies DurableDefinition<Type, typeof data>
  106. }
  107. export function ephemeral<
  108. const Type extends string,
  109. const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
  110. >(input: Omit<Input<Type, Fields>, "durable">) {
  111. const data = Schema.Struct(input.schema)
  112. return Schema.Struct({
  113. id: ID,
  114. created: DateTimeUtcFromMillis,
  115. metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
  116. type: Schema.Literal(input.type),
  117. location: optional(Location.Ref),
  118. data,
  119. })
  120. .annotate({ identifier: input.identifier ?? input.type })
  121. .pipe(
  122. statics(() => ({
  123. type: input.type,
  124. durability: "ephemeral" as const,
  125. durable: undefined,
  126. data,
  127. })),
  128. ) satisfies EphemeralDefinition<Type, typeof data>
  129. }
  130. export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
  131. return Object.freeze(definitions)
  132. }
  133. export function latest(definitions: ReadonlyArray<Definition>) {
  134. return readonlyMap(
  135. definitions.reduce((result, definition) => {
  136. const existing = result.get(definition.type)
  137. if (!existing) {
  138. result.set(definition.type, definition)
  139. return result
  140. }
  141. if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) {
  142. if (definition.durable.version > existing.durable.version) result.set(definition.type, definition)
  143. return result
  144. }
  145. if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`)
  146. return result
  147. }, new Map<string, Definition>()),
  148. )
  149. }
  150. export function versionedType(type: string, version: number) {
  151. return `${type}.${version}`
  152. }
  153. export function durableMap<const Definitions extends ReadonlyArray<Definition>>(definitions: Definitions) {
  154. return readonlyMap(
  155. definitions.reduce((result, definition) => {
  156. if (definition.durability !== "durable") return result
  157. const key = versionedType(definition.type, definition.durable.version)
  158. if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
  159. result.set(key, definition)
  160. return result
  161. }, new Map<string, DurableDefinition>()),
  162. )
  163. }
  164. function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> {
  165. const result: ReadonlyMap<Key, Value> = Object.freeze({
  166. get size() {
  167. return map.size
  168. },
  169. entries: () => map.entries(),
  170. forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) =>
  171. map.forEach((value, key) => callback.call(thisArg, value, key, result)),
  172. get: (key: Key) => map.get(key),
  173. has: (key: Key) => map.has(key),
  174. keys: () => map.keys(),
  175. values: () => map.values(),
  176. [Symbol.iterator]: () => map[Symbol.iterator](),
  177. })
  178. return result
  179. }