1
0

event.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125
  1. export * as Event from "./event"
  2. import { Schema } from "effect"
  3. import { optional } from "./schema"
  4. import { ascending } from "./identifier"
  5. import { Location } from "./location"
  6. import { statics } from "./schema"
  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. export type Definition<
  13. Type extends string = string,
  14. DataSchema extends Schema.Codec<unknown, unknown> = Schema.Codec<unknown, unknown>,
  15. > = Schema.Top & {
  16. readonly type: Type
  17. readonly durable?: {
  18. readonly version: number
  19. readonly aggregate: string
  20. }
  21. readonly data: DataSchema
  22. }
  23. export type Data<D extends Definition> = Schema.Schema.Type<D["data"]>
  24. export type Payload<D extends Definition = Definition> = {
  25. readonly id: ID
  26. readonly type: D["type"]
  27. readonly data: Data<D>
  28. readonly durable?: {
  29. readonly aggregateID: string
  30. readonly seq: number
  31. readonly version: number
  32. }
  33. readonly location?: Location.Ref
  34. readonly metadata?: Record<string, unknown>
  35. }
  36. export function define<
  37. const Type extends string,
  38. const Fields extends Readonly<Record<PropertyKey, Schema.Codec<unknown, unknown>>>,
  39. >(input: {
  40. readonly type: Type
  41. readonly durable?: {
  42. readonly version: number
  43. readonly aggregate: string
  44. }
  45. readonly schema: Fields
  46. }) {
  47. const data = Schema.Struct(input.schema)
  48. return Schema.Struct({
  49. id: ID,
  50. metadata: optional(Schema.Record(Schema.String, Schema.Unknown)),
  51. type: Schema.Literal(input.type),
  52. durable: optional(Schema.Struct({ aggregateID: Schema.String, seq: Schema.Number, version: Schema.Number })),
  53. location: optional(Location.Ref),
  54. data,
  55. })
  56. .annotate({ identifier: input.type })
  57. .pipe(
  58. statics(() => ({
  59. type: input.type,
  60. ...(input.durable === undefined ? {} : { durable: input.durable }),
  61. data,
  62. })),
  63. ) satisfies Definition<Type, typeof data>
  64. }
  65. export function inventory<const Definitions extends ReadonlyArray<Definition>>(...definitions: Definitions) {
  66. return Object.freeze(definitions)
  67. }
  68. export function latest(definitions: ReadonlyArray<Definition>) {
  69. return readonlyMap(
  70. definitions.reduce((result, definition) => {
  71. const existing = result.get(definition.type)
  72. if (!existing) {
  73. result.set(definition.type, definition)
  74. return result
  75. }
  76. if (definition.durable && existing.durable && definition.durable.version !== existing.durable.version) {
  77. if (definition.durable.version > existing.durable.version) result.set(definition.type, definition)
  78. return result
  79. }
  80. if (definition !== existing) throw new Error(`Duplicate latest event definition for ${definition.type}`)
  81. return result
  82. }, new Map<string, Definition>()),
  83. )
  84. }
  85. export function versionedType(type: string, version: number) {
  86. return `${type}.${version}`
  87. }
  88. export function durable(definitions: ReadonlyArray<Definition>) {
  89. return readonlyMap(
  90. definitions.reduce((result, definition) => {
  91. if (!definition.durable) return result
  92. const key = versionedType(definition.type, definition.durable.version)
  93. if (result.has(key)) throw new Error(`Duplicate durable event definition for ${key}`)
  94. result.set(key, definition)
  95. return result
  96. }, new Map<string, Definition>()),
  97. )
  98. }
  99. function readonlyMap<Key, Value>(map: Map<Key, Value>): ReadonlyMap<Key, Value> {
  100. const result: ReadonlyMap<Key, Value> = Object.freeze({
  101. get size() {
  102. return map.size
  103. },
  104. entries: () => map.entries(),
  105. forEach: (callback: (value: Value, key: Key, map: ReadonlyMap<Key, Value>) => void, thisArg?: unknown) =>
  106. map.forEach((value, key) => callback.call(thisArg, value, key, result)),
  107. get: (key: Key) => map.get(key),
  108. has: (key: Key) => map.has(key),
  109. keys: () => map.keys(),
  110. values: () => map.values(),
  111. [Symbol.iterator]: () => map[Symbol.iterator](),
  112. })
  113. return result
  114. }