index.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264
  1. import z from "zod"
  2. import type { ZodObject } from "zod"
  3. import { EventEmitter } from "events"
  4. import { Database, eq } from "@/storage/db"
  5. import { GlobalBus } from "@/bus/global"
  6. import { Bus as ProjectBus } from "@/bus"
  7. import { BusEvent } from "@/bus/bus-event"
  8. import { Instance } from "@/project/instance"
  9. import { EventSequenceTable, EventTable } from "./event.sql"
  10. import { WorkspaceContext } from "@/control-plane/workspace-context"
  11. import { EventID } from "./schema"
  12. import { Flag } from "@/flag/flag"
  13. export namespace SyncEvent {
  14. export type Definition = {
  15. type: string
  16. version: number
  17. aggregate: string
  18. schema: z.ZodObject
  19. // This is temporary and only exists for compatibility with bus
  20. // event definitions
  21. properties: z.ZodObject
  22. }
  23. export type Event<Def extends Definition = Definition> = {
  24. id: string
  25. seq: number
  26. aggregateID: string
  27. data: z.infer<Def["schema"]>
  28. }
  29. export type SerializedEvent<Def extends Definition = Definition> = Event<Def> & { type: string }
  30. type ProjectorFunc = (db: Database.TxOrDb, data: unknown) => void
  31. export const registry = new Map<string, Definition>()
  32. let projectors: Map<Definition, ProjectorFunc> | undefined
  33. const versions = new Map<string, number>()
  34. let frozen = false
  35. let convertEvent: (type: string, event: Event["data"]) => Promise<Record<string, unknown>> | Record<string, unknown>
  36. export function reset() {
  37. frozen = false
  38. projectors = undefined
  39. convertEvent = (_, data) => data
  40. }
  41. export function init(input: { projectors: Array<[Definition, ProjectorFunc]>; convertEvent?: typeof convertEvent }) {
  42. projectors = new Map(input.projectors)
  43. // Install all the latest event defs to the bus. We only ever emit
  44. // latest versions from code, and keep around old versions for
  45. // replaying. Replaying does not go through the bus, and it
  46. // simplifies the bus to only use unversioned latest events
  47. for (let [type, version] of versions.entries()) {
  48. let def = registry.get(versionedType(type, version))!
  49. BusEvent.define(def.type, def.properties || def.schema)
  50. }
  51. // Freeze the system so it clearly errors if events are defined
  52. // after `init` which would cause bugs
  53. frozen = true
  54. convertEvent = input.convertEvent || ((_, data) => data)
  55. }
  56. export function versionedType<A extends string>(type: A): A
  57. export function versionedType<A extends string, B extends number>(type: A, version: B): `${A}/${B}`
  58. export function versionedType(type: string, version?: number) {
  59. return version ? `${type}.${version}` : type
  60. }
  61. export function define<
  62. Type extends string,
  63. Agg extends string,
  64. Schema extends ZodObject<Record<Agg, z.ZodType<string>>>,
  65. BusSchema extends ZodObject = Schema,
  66. >(input: { type: Type; version: number; aggregate: Agg; schema: Schema; busSchema?: BusSchema }) {
  67. if (frozen) {
  68. throw new Error("Error defining sync event: sync system has been frozen")
  69. }
  70. const def = {
  71. type: input.type,
  72. version: input.version,
  73. aggregate: input.aggregate,
  74. schema: input.schema,
  75. properties: input.busSchema ? input.busSchema : input.schema,
  76. }
  77. versions.set(def.type, Math.max(def.version, versions.get(def.type) || 0))
  78. registry.set(versionedType(def.type, def.version), def)
  79. return def
  80. }
  81. export function project<Def extends Definition>(
  82. def: Def,
  83. func: (db: Database.TxOrDb, data: Event<Def>["data"]) => void,
  84. ): [Definition, ProjectorFunc] {
  85. return [def, func as ProjectorFunc]
  86. }
  87. function process<Def extends Definition>(def: Def, event: Event<Def>, options: { publish: boolean }) {
  88. if (projectors == null) {
  89. throw new Error("No projectors available. Call `SyncEvent.init` to install projectors")
  90. }
  91. const projector = projectors.get(def)
  92. if (!projector) {
  93. throw new Error(`Projector not found for event: ${def.type}`)
  94. }
  95. // idempotent: need to ignore any events already logged
  96. Database.transaction((tx) => {
  97. projector(tx, event.data)
  98. if (Flag.OPENCODE_EXPERIMENTAL_WORKSPACES) {
  99. tx.insert(EventSequenceTable)
  100. .values({
  101. aggregate_id: event.aggregateID,
  102. seq: event.seq,
  103. })
  104. .onConflictDoUpdate({
  105. target: EventSequenceTable.aggregate_id,
  106. set: { seq: event.seq },
  107. })
  108. .run()
  109. tx.insert(EventTable)
  110. .values({
  111. id: event.id,
  112. seq: event.seq,
  113. aggregate_id: event.aggregateID,
  114. type: versionedType(def.type, def.version),
  115. data: event.data as Record<string, unknown>,
  116. })
  117. .run()
  118. }
  119. Database.effect(() => {
  120. if (options?.publish) {
  121. const result = convertEvent(def.type, event.data)
  122. if (result instanceof Promise) {
  123. result.then((data) => {
  124. ProjectBus.publish({ type: def.type, properties: def.schema }, data)
  125. })
  126. } else {
  127. ProjectBus.publish({ type: def.type, properties: def.schema }, result)
  128. }
  129. GlobalBus.emit("event", {
  130. directory: Instance.directory,
  131. project: Instance.project.id,
  132. workspace: WorkspaceContext.workspaceID,
  133. payload: {
  134. type: "sync",
  135. name: versionedType(def.type, def.version),
  136. ...event,
  137. },
  138. })
  139. }
  140. })
  141. })
  142. }
  143. // TODO:
  144. //
  145. // * Support applying multiple events at one time. One transaction,
  146. // and it validets all the sequence ids
  147. // * when loading events from db, apply zod validation to ensure shape
  148. export function replay(event: SerializedEvent, options?: { publish: boolean }) {
  149. const def = registry.get(event.type)
  150. if (!def) {
  151. throw new Error(`Unknown event type: ${event.type}`)
  152. }
  153. const row = Database.use((db) =>
  154. db
  155. .select({ seq: EventSequenceTable.seq })
  156. .from(EventSequenceTable)
  157. .where(eq(EventSequenceTable.aggregate_id, event.aggregateID))
  158. .get(),
  159. )
  160. const latest = row?.seq ?? -1
  161. if (event.seq <= latest) {
  162. return
  163. }
  164. const expected = latest + 1
  165. if (event.seq !== expected) {
  166. throw new Error(`Sequence mismatch for aggregate "${event.aggregateID}": expected ${expected}, got ${event.seq}`)
  167. }
  168. process(def, event, { publish: !!options?.publish })
  169. }
  170. export function run<Def extends Definition>(def: Def, data: Event<Def>["data"], options?: { publish?: boolean }) {
  171. const agg = (data as Record<string, string>)[def.aggregate]
  172. // This should never happen: we've enforced it via typescript in
  173. // the definition
  174. if (agg == null) {
  175. throw new Error(`SyncEvent.run: "${def.aggregate}" required but not found: ${JSON.stringify(data)}`)
  176. }
  177. if (def.version !== versions.get(def.type)) {
  178. throw new Error(`SyncEvent.run: running old versions of events is not allowed: ${def.type}`)
  179. }
  180. const { publish = true } = options || {}
  181. // Note that this is an "immediate" transaction which is critical.
  182. // We need to make sure we can safely read and write with nothing
  183. // else changing the data from under us
  184. Database.transaction(
  185. (tx) => {
  186. const id = EventID.ascending()
  187. const row = tx
  188. .select({ seq: EventSequenceTable.seq })
  189. .from(EventSequenceTable)
  190. .where(eq(EventSequenceTable.aggregate_id, agg))
  191. .get()
  192. const seq = row?.seq != null ? row.seq + 1 : 0
  193. const event = { id, seq, aggregateID: agg, data }
  194. process(def, event, { publish })
  195. },
  196. {
  197. behavior: "immediate",
  198. },
  199. )
  200. }
  201. export function remove(aggregateID: string) {
  202. Database.transaction((tx) => {
  203. tx.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
  204. tx.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
  205. })
  206. }
  207. export function payloads() {
  208. return registry
  209. .entries()
  210. .map(([type, def]) => {
  211. return z
  212. .object({
  213. type: z.literal("sync"),
  214. name: z.literal(type),
  215. id: z.string(),
  216. seq: z.number(),
  217. aggregateID: z.literal(def.aggregate),
  218. data: def.schema,
  219. })
  220. .meta({
  221. ref: "SyncEvent" + "." + def.type,
  222. })
  223. })
  224. .toArray()
  225. }
  226. }