bus.test.ts 44 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321
  1. import { describe, expect } from "bun:test"
  2. import { Cause, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Ref, Schema, Stream } from "effect"
  3. import { Bus } from "@opencode-ai/core/bus"
  4. import { Event } from "@opencode-ai/schema/event"
  5. import { Session } from "@opencode-ai/schema/session"
  6. import { SessionEvent } from "@opencode-ai/schema/session-event"
  7. import { SessionV1 } from "@opencode-ai/schema/session-v1"
  8. import { Database } from "@opencode-ai/core/database/database"
  9. import { AppNodeBuilder } from "@opencode-ai/core/effect/app-node-builder"
  10. import { LayerNode } from "@opencode-ai/util/effect/layer-node"
  11. import { EventSequenceTable, EventTable } from "@opencode-ai/core/event/sql"
  12. import { Location } from "@opencode-ai/core/location"
  13. import { AbsolutePath } from "@opencode-ai/core/schema"
  14. import { Workspace } from "@opencode-ai/core/workspace"
  15. import { eq } from "drizzle-orm"
  16. import { location } from "./fixture/location"
  17. import { testEffect } from "./lib/effect"
  18. const locationLayer = Layer.succeed(
  19. Location.Service,
  20. Location.Service.of(
  21. location({ directory: AbsolutePath.make("project"), workspaceID: Workspace.ID.make("wrk_test") }),
  22. ),
  23. )
  24. const Message = Bus.ephemeral({
  25. type: "test.message",
  26. schema: {
  27. text: Schema.String,
  28. },
  29. })
  30. const SyncMessage = Bus.durable({
  31. type: "test.sync",
  32. durable: {
  33. version: 1,
  34. aggregate: "id",
  35. },
  36. schema: {
  37. id: Schema.String,
  38. text: Schema.String,
  39. },
  40. })
  41. const SyncSent = Bus.durable({
  42. type: "test.sent",
  43. durable: {
  44. version: 1,
  45. aggregate: "messageID",
  46. },
  47. schema: {
  48. messageID: Schema.String,
  49. text: Schema.String,
  50. },
  51. })
  52. const VersionedMessageV1 = Bus.durable({
  53. type: "test.versioned",
  54. durable: { version: 1, aggregate: "id" },
  55. schema: { id: Schema.String },
  56. })
  57. const VersionedMessageV2 = Bus.durable({
  58. type: "test.versioned",
  59. durable: { version: 2, aggregate: "id" },
  60. schema: { id: Schema.String },
  61. })
  62. const GlobalMessage = Bus.ephemeral({
  63. type: "test.global",
  64. schema: {
  65. text: Schema.String,
  66. },
  67. })
  68. const CountMessage = Bus.ephemeral({
  69. type: "test.count",
  70. schema: {
  71. count: Schema.Number,
  72. },
  73. })
  74. const VersionedMessage = Bus.durable({
  75. type: "test.versioned",
  76. durable: {
  77. version: 2,
  78. aggregate: "id",
  79. },
  80. schema: {
  81. id: Schema.String,
  82. text: Schema.String,
  83. },
  84. })
  85. const DurableMessage = SessionV1.Event.MessageRemoved
  86. const durableData = (sessionID: Session.ID, text: string) => ({
  87. sessionID,
  88. messageID: SessionV1.MessageID.ascending(`msg_${text}`),
  89. })
  90. /** Followed log read without markers: the old `durable` stream shape. */
  91. const tail = (bus: Bus.Interface, input: { aggregateID: string; after?: number }) =>
  92. bus.log({ ...input, follow: true }).pipe(Stream.filter((item): item is Event.Payload => !Bus.isSynced(item)))
  93. const it = testEffect(
  94. AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node, Location.node]), [[Location.node, locationLayer]]),
  95. )
  96. const itWithoutLocation = testEffect(AppNodeBuilder.build(LayerNode.group([Database.node, Bus.node])))
  97. describe("Bus", () => {
  98. it.effect("subscribes to multiple event definitions with a discriminated payload union", () =>
  99. Effect.gen(function* () {
  100. const bus = yield* Bus.Service
  101. // @ts-expect-error multi-definition subscriptions require at least one definition
  102. bus.subscribe([])
  103. const fiber = yield* bus
  104. .subscribe([Message, CountMessage])
  105. .pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
  106. yield* Effect.yieldNow
  107. yield* bus.publish(Message, { text: "hello" })
  108. yield* bus.publish(CountMessage, { count: 2 })
  109. const received = Array.from(yield* Fiber.join(fiber)).map((event) =>
  110. event.type === "test.message" ? event.data.text : event.data.count,
  111. )
  112. expect(received).toEqual(["hello", 2])
  113. }),
  114. )
  115. it.effect("publishes events with the current location", () =>
  116. Effect.gen(function* () {
  117. const bus = yield* Bus.Service
  118. const fiber = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  119. yield* Effect.yieldNow
  120. const event = yield* bus.publish(Message, { text: "hello" })
  121. const received = Array.from(yield* Fiber.join(fiber))
  122. expect(received).toEqual([event])
  123. expect(event.type).toBe("test.message")
  124. expect(event).not.toHaveProperty("version")
  125. expect(event.data).toEqual({ text: "hello" })
  126. expect(event.location).toEqual({
  127. directory: AbsolutePath.make("project"),
  128. workspaceID: Workspace.ID.make("wrk_test"),
  129. })
  130. }),
  131. )
  132. itWithoutLocation.effect("omits location when no location is available", () =>
  133. Effect.gen(function* () {
  134. const bus = yield* Bus.Service
  135. const event = yield* bus.publish(GlobalMessage, { text: "hello" })
  136. expect(event).not.toHaveProperty("location")
  137. expect(event.type).toBe("test.global")
  138. }),
  139. )
  140. it.effect("publishes definition version", () =>
  141. Effect.gen(function* () {
  142. const bus = yield* Bus.Service
  143. const event = yield* bus.publish(VersionedMessage, { id: "one", text: "hello" })
  144. expect(event.type).toBe("test.versioned")
  145. expect(event.durable?.version).toBe(Event.Version.make(2))
  146. }),
  147. )
  148. it.effect("selects the latest durable definition independent of declaration order", () =>
  149. Effect.sync(() => {
  150. const latest = Bus.durable({
  151. type: "test.out-of-order",
  152. durable: { version: 2, aggregate: "id" },
  153. schema: { id: Schema.String },
  154. })
  155. const historical = Bus.durable({
  156. type: "test.out-of-order",
  157. durable: { version: 1, aggregate: "id" },
  158. schema: { id: Schema.String },
  159. })
  160. expect(Event.latest([latest, historical]).get("test.out-of-order")).toBe(latest)
  161. expect(Event.latest([historical, latest]).get("test.out-of-order")).toBe(latest)
  162. }),
  163. )
  164. it.effect("publishes to typed and wildcard subscriptions", () =>
  165. Effect.gen(function* () {
  166. const bus = yield* Bus.Service
  167. const typed = yield* bus.subscribe(Message).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  168. const wildcard = yield* bus.subscribe().pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  169. yield* Effect.yieldNow
  170. const event = yield* bus.publish(Message, { text: "hello" })
  171. expect(Array.from(yield* Fiber.join(typed))).toEqual([event])
  172. expect(Array.from(yield* Fiber.join(wildcard))).toEqual([event])
  173. }),
  174. )
  175. it.effect("runs projectors inline", () =>
  176. Effect.gen(function* () {
  177. const bus = yield* Bus.Service
  178. const received = new Array<Event.Payload>()
  179. yield* bus.project(SyncMessage, (event) =>
  180. Effect.sync(() => {
  181. received.push(event)
  182. }),
  183. )
  184. const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" })
  185. yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" })
  186. expect(received[0]).toEqual(event)
  187. expect(received[1]?.data).toEqual({ id: "one", text: "after unsubscribe" })
  188. }),
  189. )
  190. it.effect("commits local operational state inside a new durable event transaction", () =>
  191. Effect.gen(function* () {
  192. const bus = yield* Bus.Service
  193. const received = new Array<string>()
  194. const aggregateID = Event.ID.create()
  195. yield* bus.project(SyncMessage, () => Effect.sync(() => received.push("projector")))
  196. yield* bus.publish(
  197. SyncMessage,
  198. { id: aggregateID, text: "hello" },
  199. { commit: (seq) => Effect.sync(() => received.push(`commit:${seq}`)) },
  200. )
  201. expect(received).toEqual(["projector", "commit:0"])
  202. }),
  203. )
  204. it.effect("rolls back the durable event and projector when the local commit fails", () =>
  205. Effect.gen(function* () {
  206. const bus = yield* Bus.Service
  207. const { db } = yield* Database.Service
  208. const aggregateID = Event.ID.create()
  209. yield* db.run("CREATE TABLE IF NOT EXISTS event_commit_probe (value text NOT NULL)")
  210. yield* db.run("DELETE FROM event_commit_probe")
  211. yield* bus.project(SyncMessage, () =>
  212. db.run("INSERT INTO event_commit_probe (value) VALUES ('projected')").pipe(Effect.orDie, Effect.asVoid),
  213. )
  214. const exit = yield* bus
  215. .publish(SyncMessage, { id: aggregateID, text: "hello" }, { commit: () => Effect.die("commit failed") })
  216. .pipe(Effect.exit)
  217. expect(String(exit)).toContain("commit failed")
  218. expect(yield* db.all("SELECT value FROM event_commit_probe")).toEqual([])
  219. expect(yield* db.select().from(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).all()).toEqual([])
  220. expect(
  221. yield* db.select().from(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).all(),
  222. ).toEqual([])
  223. }),
  224. )
  225. it.effect("rejects local commit hooks on live-only events", () =>
  226. Effect.gen(function* () {
  227. const bus = yield* Bus.Service
  228. const exit = yield* bus.publish(Message, { text: "hello" }, { commit: () => Effect.void }).pipe(Effect.exit)
  229. expect(String(exit)).toContain("Local commit hooks require a durable event")
  230. }),
  231. )
  232. it.effect("runs projectors before publishing to streams", () =>
  233. Effect.gen(function* () {
  234. const bus = yield* Bus.Service
  235. const received = new Array<string>()
  236. const fiber = yield* bus.subscribe().pipe(
  237. Stream.take(1),
  238. Stream.runForEach(() => Effect.sync(() => received.push("stream"))),
  239. Effect.forkScoped,
  240. )
  241. yield* bus.project(SyncMessage, (event) =>
  242. Effect.sync(() => {
  243. received.push(event.type)
  244. }),
  245. )
  246. yield* Effect.yieldNow
  247. yield* bus.publish(SyncMessage, { id: "one", text: "hello" })
  248. yield* Fiber.join(fiber)
  249. expect(received).toEqual([SyncMessage.type, "stream"])
  250. }),
  251. )
  252. it.effect("runs listeners inline after projectors", () =>
  253. Effect.gen(function* () {
  254. const bus = yield* Bus.Service
  255. const received = new Array<string>()
  256. yield* bus.project(SyncMessage, () =>
  257. Effect.sync(() => {
  258. received.push("projector")
  259. }),
  260. )
  261. const unsubscribe = yield* bus.listen(() =>
  262. Effect.sync(() => {
  263. received.push("listener")
  264. }),
  265. )
  266. yield* bus.publish(SyncMessage, { id: "one", text: "hello" })
  267. yield* unsubscribe
  268. yield* bus.publish(SyncMessage, { id: "one", text: "after unsubscribe" })
  269. expect(received).toEqual(["projector", "listener", "projector"])
  270. }),
  271. )
  272. it.effect("isolates observer defects after durable events commit", () =>
  273. Effect.gen(function* () {
  274. const bus = yield* Bus.Service
  275. const received = new Array<string>()
  276. yield* bus.listen(() => {
  277. throw new Error("listener defect")
  278. })
  279. yield* bus.listen((event) =>
  280. Effect.sync(() => {
  281. received.push(event.type)
  282. }),
  283. )
  284. const event = yield* bus.publish(SyncMessage, { id: "one", text: "hello" })
  285. expect(received).toEqual([SyncMessage.type])
  286. expect(event.durable?.seq).toBeNumber()
  287. }),
  288. )
  289. it.effect("notifies global listeners only after a durable event is committed", () =>
  290. Effect.gen(function* () {
  291. const bus = yield* Bus.Service
  292. const { db } = yield* Database.Service
  293. const aggregateID = Event.ID.create()
  294. const observed = new Array<{ id: string; seq: number }>()
  295. yield* bus.listen((event) =>
  296. event.type !== SyncMessage.type
  297. ? Effect.void
  298. : db
  299. .select({ id: EventTable.id, seq: EventTable.seq })
  300. .from(EventTable)
  301. .where(eq(EventTable.id, event.id))
  302. .get()
  303. .pipe(
  304. Effect.orDie,
  305. Effect.tap((row) =>
  306. Effect.sync(() => {
  307. if (row) observed.push(row)
  308. }),
  309. ),
  310. Effect.asVoid,
  311. ),
  312. )
  313. const event = yield* bus.publish(SyncMessage, { id: aggregateID, text: "committed" })
  314. if (!event.durable) throw new Error("Expected durable event metadata")
  315. expect(observed).toEqual([{ id: event.id, seq: event.durable.seq }])
  316. }),
  317. )
  318. it.effect("preserves observer interruption", () =>
  319. Effect.gen(function* () {
  320. const bus = yield* Bus.Service
  321. const { db } = yield* Database.Service
  322. yield* bus.listen(() => Effect.interrupt)
  323. const exit = yield* bus.publish(SyncMessage, { id: "interrupted", text: "hello" }).pipe(Effect.exit)
  324. const committed = yield* db
  325. .select({ id: EventTable.id })
  326. .from(EventTable)
  327. .where(eq(EventTable.aggregate_id, "interrupted"))
  328. .get()
  329. .pipe(Effect.orDie)
  330. expect(Exit.isFailure(exit) && Cause.hasInterrupts(exit.cause)).toBeTrue()
  331. expect(committed).toBeDefined()
  332. }),
  333. )
  334. it.effect("keeps live-only listener defects fail-fast", () =>
  335. Effect.gen(function* () {
  336. const bus = yield* Bus.Service
  337. const defect = new Error("listener defect")
  338. yield* bus.listen(() => Effect.die(defect))
  339. expect(yield* bus.publish(Message, { text: "hello" }).pipe(Effect.catchDefect(Effect.succeed))).toBe(defect)
  340. }),
  341. )
  342. it.effect("inserts durable event rows on publish", () =>
  343. Effect.gen(function* () {
  344. const bus = yield* Bus.Service
  345. const { db } = yield* Database.Service
  346. const aggregateID = Event.ID.create()
  347. yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" })
  348. const rows = yield* db
  349. .select()
  350. .from(EventTable)
  351. .where(eq(EventTable.aggregate_id, aggregateID))
  352. .all()
  353. .pipe(Effect.orDie)
  354. expect(rows).toHaveLength(1)
  355. expect(rows[0]?.type).toBe(Bus.versionedType(SyncMessage.type, 1))
  356. expect(rows[0]?.aggregate_id).toBe(aggregateID)
  357. }),
  358. )
  359. it.effect("increments durable event seq per aggregate", () =>
  360. Effect.gen(function* () {
  361. const bus = yield* Bus.Service
  362. const { db } = yield* Database.Service
  363. const aggregateID = Event.ID.create()
  364. yield* bus.publish(SyncMessage, { id: aggregateID, text: "first" })
  365. yield* bus.publish(SyncMessage, { id: aggregateID, text: "second" })
  366. const rows = yield* db
  367. .select()
  368. .from(EventTable)
  369. .where(eq(EventTable.aggregate_id, aggregateID))
  370. .all()
  371. .pipe(Effect.orDie)
  372. expect(rows.map((row) => row.seq)).toEqual([0, 1])
  373. }),
  374. )
  375. it.effect("replays durable aggregate events after a sequence and tails new events", () =>
  376. Effect.gen(function* () {
  377. const bus = yield* Bus.Service
  378. const aggregateID = Session.ID.create()
  379. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  380. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  381. const fiber = yield* tail(bus, { aggregateID, after: 0 }).pipe(
  382. Stream.take(2),
  383. Stream.runCollect,
  384. Effect.forkScoped,
  385. )
  386. yield* Effect.yieldNow
  387. yield* bus.publish(DurableMessage, durableData(aggregateID, "two"))
  388. expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
  389. [1, durableData(aggregateID, "one")],
  390. [2, durableData(aggregateID, "two")],
  391. ])
  392. }),
  393. )
  394. it.effect("catches durable aggregate events published during replay handoff", () =>
  395. Effect.gen(function* () {
  396. const bus = yield* Bus.Service
  397. const aggregateID = Session.ID.create()
  398. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  399. const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(2), Stream.runCollect, Effect.forkScoped)
  400. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  401. expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
  402. [0, durableData(aggregateID, "zero")],
  403. [1, durableData(aggregateID, "one")],
  404. ])
  405. }),
  406. )
  407. it.effect("retains a durable wake committed while historical replay is paused", () =>
  408. Effect.gen(function* () {
  409. const readStarted = yield* Deferred.make<void>()
  410. const continueRead = yield* Deferred.make<void>()
  411. let pause = true
  412. const eventLayer = Bus.layerWith({
  413. beforeAggregateRead: () =>
  414. pause
  415. ? Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(continueRead)))
  416. : Effect.void,
  417. }).pipe(Layer.provide(LayerNode.compile(Database.node)))
  418. yield* Effect.gen(function* () {
  419. const bus = yield* Bus.Service
  420. const aggregateID = Session.ID.create()
  421. const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  422. yield* Deferred.await(readStarted)
  423. pause = false
  424. yield* bus.publish(DurableMessage, durableData(aggregateID, "during handoff"))
  425. yield* Deferred.succeed(continueRead, undefined)
  426. expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual([
  427. [0, durableData(aggregateID, "during handoff")],
  428. ])
  429. }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer)))
  430. }),
  431. )
  432. it.effect("coalesces durable aggregate wakes while draining every committed event", () =>
  433. Effect.gen(function* () {
  434. const bus = yield* Bus.Service
  435. const aggregateID = Session.ID.create()
  436. const count = 64
  437. const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(count), Stream.runCollect, Effect.forkScoped)
  438. yield* Effect.yieldNow
  439. for (let index = 0; index < count; index++) {
  440. yield* bus.publish(DurableMessage, durableData(aggregateID, String(index)))
  441. }
  442. expect(Array.from(yield* Fiber.join(fiber)).map((event) => [event.durable?.seq, event.data])).toEqual(
  443. Array.from({ length: count }, (_, index) => [index, durableData(aggregateID, String(index))]),
  444. )
  445. }),
  446. )
  447. it.effect("omits live-only events from durable aggregate streams", () =>
  448. Effect.gen(function* () {
  449. const bus = yield* Bus.Service
  450. const aggregateID = Session.ID.create()
  451. const fiber = yield* tail(bus, { aggregateID }).pipe(Stream.take(1), Stream.runCollect, Effect.forkScoped)
  452. yield* Effect.yieldNow
  453. yield* bus.publish(Message, { text: "live only" })
  454. yield* bus.publish(DurableMessage, durableData(aggregateID, "durable"))
  455. expect(Array.from(yield* Fiber.join(fiber)).map((event) => event.type)).toEqual([DurableMessage.type])
  456. }),
  457. )
  458. it.effect("uses custom sync aggregate field", () =>
  459. Effect.gen(function* () {
  460. const bus = yield* Bus.Service
  461. const { db } = yield* Database.Service
  462. const aggregateID = Event.ID.create()
  463. yield* bus.publish(SyncSent, { messageID: aggregateID, text: "sent" })
  464. const rows = yield* db
  465. .select()
  466. .from(EventTable)
  467. .where(eq(EventTable.aggregate_id, aggregateID))
  468. .all()
  469. .pipe(Effect.orDie)
  470. expect(rows).toHaveLength(1)
  471. expect(rows[0]?.aggregate_id).toBe(aggregateID)
  472. }),
  473. )
  474. it.effect("replays durable events through projectors", () =>
  475. Effect.gen(function* () {
  476. const bus = yield* Bus.Service
  477. const received = new Array<Event.Payload>()
  478. yield* bus.project(DurableMessage, (event) =>
  479. Effect.sync(() => {
  480. received.push(event)
  481. }),
  482. )
  483. const aggregateID = Session.ID.create()
  484. yield* bus.replay({
  485. id: Event.ID.create(),
  486. created: DateTime.makeUnsafe(0),
  487. type: Bus.versionedType(DurableMessage.type, 1),
  488. seq: 0,
  489. aggregateID,
  490. data: durableData(aggregateID, "hello"),
  491. })
  492. expect(received[0]?.type).toBe(DurableMessage.type)
  493. expect(received[0]?.data).toEqual(durableData(aggregateID, "hello"))
  494. }),
  495. )
  496. it.effect("replay inserts external event rows", () =>
  497. Effect.gen(function* () {
  498. const bus = yield* Bus.Service
  499. const { db } = yield* Database.Service
  500. const aggregateID = Session.ID.create()
  501. yield* bus.replay({
  502. id: Event.ID.create(),
  503. created: DateTime.makeUnsafe(0),
  504. type: Bus.versionedType(DurableMessage.type, 1),
  505. seq: 0,
  506. aggregateID,
  507. data: durableData(aggregateID, "replayed"),
  508. })
  509. const rows = yield* db
  510. .select()
  511. .from(EventTable)
  512. .where(eq(EventTable.aggregate_id, aggregateID))
  513. .all()
  514. .pipe(Effect.orDie)
  515. expect(rows).toHaveLength(1)
  516. expect(rows[0]?.aggregate_id).toBe(aggregateID)
  517. }),
  518. )
  519. it.effect(
  520. "replay rejects an envelope aggregate that differs from its payload without mutating the payload aggregate",
  521. () =>
  522. Effect.gen(function* () {
  523. const bus = yield* Bus.Service
  524. const { db } = yield* Database.Service
  525. const envelopeAggregateID = Session.ID.create()
  526. const payloadAggregateID = Session.ID.create()
  527. const received = new Array<Event.Payload>()
  528. yield* bus.publish(DurableMessage, durableData(payloadAggregateID, "seed"))
  529. yield* bus.project(DurableMessage, (event) =>
  530. Effect.sync(() => {
  531. received.push(event)
  532. }),
  533. )
  534. const exit = yield* bus
  535. .replay({
  536. id: Event.ID.create(),
  537. created: DateTime.makeUnsafe(0),
  538. type: Bus.versionedType(DurableMessage.type, 1),
  539. seq: 1,
  540. aggregateID: envelopeAggregateID,
  541. data: durableData(payloadAggregateID, "replayed"),
  542. })
  543. .pipe(Effect.exit)
  544. const rows = yield* db
  545. .select()
  546. .from(EventTable)
  547. .where(eq(EventTable.aggregate_id, payloadAggregateID))
  548. .all()
  549. .pipe(Effect.orDie)
  550. const sequence = yield* db
  551. .select({ seq: EventSequenceTable.seq })
  552. .from(EventSequenceTable)
  553. .where(eq(EventSequenceTable.aggregate_id, payloadAggregateID))
  554. .get()
  555. .pipe(Effect.orDie)
  556. expect(String(exit)).toContain("Aggregate mismatch")
  557. expect(received).toHaveLength(0)
  558. expect(rows).toHaveLength(1)
  559. expect(sequence).toEqual({ seq: 0 })
  560. }),
  561. )
  562. it.effect("replay defects on sequence mismatch", () =>
  563. Effect.gen(function* () {
  564. const bus = yield* Bus.Service
  565. const aggregateID = Session.ID.create()
  566. yield* bus.replay({
  567. id: Event.ID.create(),
  568. created: DateTime.makeUnsafe(0),
  569. type: Bus.versionedType(DurableMessage.type, 1),
  570. seq: 0,
  571. aggregateID,
  572. data: durableData(aggregateID, "first"),
  573. })
  574. const exit = yield* bus
  575. .replay({
  576. id: Event.ID.create(),
  577. created: DateTime.makeUnsafe(0),
  578. type: Bus.versionedType(DurableMessage.type, 1),
  579. seq: 5,
  580. aggregateID,
  581. data: durableData(aggregateID, "bad"),
  582. })
  583. .pipe(Effect.exit)
  584. expect(String(exit)).toContain("Sequence mismatch")
  585. }),
  586. )
  587. it.effect("replay decodes synchronized transformed values before projection", () =>
  588. Effect.gen(function* () {
  589. const bus = yield* Bus.Service
  590. const aggregateID = Session.ID.create()
  591. const received = new Array<typeof SessionEvent.InstructionsUpdated.Type>()
  592. yield* bus.project(SessionEvent.InstructionsUpdated, (event) =>
  593. Effect.sync(() => {
  594. received.push(event)
  595. }),
  596. )
  597. yield* bus.replay({
  598. id: Event.ID.create(),
  599. created: DateTime.makeUnsafe(0),
  600. type: Bus.versionedType(SessionEvent.InstructionsUpdated.type, 2),
  601. seq: 0,
  602. aggregateID,
  603. data: { sessionID: aggregateID, delta: { "core/context": "0".repeat(64) } },
  604. })
  605. expect(received[0]?.created).toEqual(DateTime.makeUnsafe(0))
  606. }),
  607. )
  608. it.effect("dispatches durable projectors by exact event version", () =>
  609. Effect.gen(function* () {
  610. const bus = yield* Bus.Service
  611. const aggregateID = Session.ID.create()
  612. const received = new Array<typeof VersionedMessageV2.Type>()
  613. yield* bus.project(VersionedMessageV2, (event) =>
  614. Effect.sync(() => {
  615. received.push(event)
  616. }),
  617. )
  618. yield* bus.publish(VersionedMessageV1, { id: aggregateID })
  619. yield* bus.publish(VersionedMessageV2, { id: aggregateID })
  620. expect(received).toHaveLength(1)
  621. expect(received[0]?.durable.version).toBe(Event.Version.make(2))
  622. }),
  623. )
  624. it.effect("replay defects on unknown event type", () =>
  625. Effect.gen(function* () {
  626. const bus = yield* Bus.Service
  627. const exit = yield* bus
  628. .replay({
  629. id: Event.ID.create(),
  630. created: DateTime.makeUnsafe(0),
  631. type: "unknown.event.1",
  632. seq: 0,
  633. aggregateID: Event.ID.create(),
  634. data: {},
  635. })
  636. .pipe(Effect.exit)
  637. expect(String(exit)).toContain("Unknown durable event type")
  638. }),
  639. )
  640. it.effect("replayAll validates contiguous aggregate events", () =>
  641. Effect.gen(function* () {
  642. const bus = yield* Bus.Service
  643. const aggregateID = Session.ID.create()
  644. const source = yield* bus.replayAll([
  645. {
  646. id: Event.ID.create(),
  647. created: DateTime.makeUnsafe(0),
  648. type: Bus.versionedType(DurableMessage.type, 1),
  649. seq: 0,
  650. aggregateID,
  651. data: durableData(aggregateID, "one"),
  652. },
  653. {
  654. id: Event.ID.create(),
  655. created: DateTime.makeUnsafe(0),
  656. type: Bus.versionedType(DurableMessage.type, 1),
  657. seq: 1,
  658. aggregateID,
  659. data: durableData(aggregateID, "two"),
  660. },
  661. ])
  662. expect(source).toBe(aggregateID)
  663. }),
  664. )
  665. it.effect("replayAll accepts later chunks after the first batch", () =>
  666. Effect.gen(function* () {
  667. const bus = yield* Bus.Service
  668. const { db } = yield* Database.Service
  669. const aggregateID = Session.ID.create()
  670. const one = yield* bus.replayAll([
  671. {
  672. id: Event.ID.create(),
  673. created: DateTime.makeUnsafe(0),
  674. type: Bus.versionedType(DurableMessage.type, 1),
  675. seq: 0,
  676. aggregateID,
  677. data: durableData(aggregateID, "one"),
  678. },
  679. {
  680. id: Event.ID.create(),
  681. created: DateTime.makeUnsafe(0),
  682. type: Bus.versionedType(DurableMessage.type, 1),
  683. seq: 1,
  684. aggregateID,
  685. data: durableData(aggregateID, "two"),
  686. },
  687. ])
  688. const two = yield* bus.replayAll([
  689. {
  690. id: Event.ID.create(),
  691. created: DateTime.makeUnsafe(0),
  692. type: Bus.versionedType(DurableMessage.type, 1),
  693. seq: 2,
  694. aggregateID,
  695. data: durableData(aggregateID, "three"),
  696. },
  697. {
  698. id: Event.ID.create(),
  699. created: DateTime.makeUnsafe(0),
  700. type: Bus.versionedType(DurableMessage.type, 1),
  701. seq: 3,
  702. aggregateID,
  703. data: durableData(aggregateID, "four"),
  704. },
  705. ])
  706. const rows = yield* db
  707. .select()
  708. .from(EventTable)
  709. .where(eq(EventTable.aggregate_id, aggregateID))
  710. .all()
  711. .pipe(Effect.orDie)
  712. expect(one).toBe(aggregateID)
  713. expect(two).toBe(aggregateID)
  714. expect(rows.map((row) => row.seq)).toEqual([0, 1, 2, 3])
  715. }),
  716. )
  717. it.effect("claim fences replay owners", () =>
  718. Effect.gen(function* () {
  719. const bus = yield* Bus.Service
  720. const received = new Array<Event.Payload>()
  721. const aggregateID = Session.ID.create()
  722. yield* bus.publish(DurableMessage, durableData(aggregateID, "seed"))
  723. yield* bus.claim(aggregateID, "owner-a")
  724. yield* bus.project(DurableMessage, (event) =>
  725. Effect.sync(() => {
  726. received.push(event)
  727. }),
  728. )
  729. yield* bus.replay(
  730. {
  731. id: Event.ID.create(),
  732. created: DateTime.makeUnsafe(0),
  733. type: Bus.versionedType(DurableMessage.type, 1),
  734. seq: 1,
  735. aggregateID,
  736. data: durableData(aggregateID, "ignored"),
  737. },
  738. { ownerID: "owner-b" },
  739. )
  740. expect(received).toHaveLength(0)
  741. }),
  742. )
  743. it.effect("strict owner fences exact replay", () =>
  744. Effect.gen(function* () {
  745. const bus = yield* Bus.Service
  746. const aggregateID = Session.ID.create()
  747. const id = Event.ID.create()
  748. const replayed = {
  749. id,
  750. created: DateTime.makeUnsafe(0),
  751. type: Bus.versionedType(DurableMessage.type, 1),
  752. seq: 0,
  753. aggregateID,
  754. data: durableData(aggregateID, "owned"),
  755. }
  756. yield* bus.replay(replayed, { ownerID: "owner-a" })
  757. const exit = yield* bus.replay(replayed, { ownerID: "owner-b", strictOwner: true }).pipe(Effect.exit)
  758. expect(String(exit)).toContain("Replay owner mismatch")
  759. }),
  760. )
  761. it.effect("exact replay claims an unowned aggregate", () =>
  762. Effect.gen(function* () {
  763. const bus = yield* Bus.Service
  764. const { db } = yield* Database.Service
  765. const aggregateID = Session.ID.create()
  766. const published = yield* bus.publish(DurableMessage, durableData(aggregateID, "owned"))
  767. const replayed = {
  768. id: published.id,
  769. created: published.created,
  770. type: Bus.versionedType(DurableMessage.type, 1),
  771. seq: published.durable!.seq,
  772. aggregateID,
  773. data: published.data,
  774. }
  775. yield* bus.replay(replayed, { ownerID: "owner-a", strictOwner: true })
  776. const row = yield* db
  777. .select({ ownerID: EventSequenceTable.owner_id })
  778. .from(EventSequenceTable)
  779. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  780. .get()
  781. .pipe(Effect.orDie)
  782. expect(row?.ownerID).toBe("owner-a")
  783. const exit = yield* bus
  784. .replay(
  785. { ...replayed, id: Event.ID.create(), seq: 1, data: durableData(aggregateID, "conflict") },
  786. { ownerID: "owner-b", strictOwner: true },
  787. )
  788. .pipe(Effect.exit)
  789. expect(String(exit)).toContain("Replay owner mismatch")
  790. }),
  791. )
  792. it.effect("replay with owner claims an unowned sequence", () =>
  793. Effect.gen(function* () {
  794. const bus = yield* Bus.Service
  795. const { db } = yield* Database.Service
  796. const aggregateID = Session.ID.create()
  797. yield* bus.replay(
  798. {
  799. id: Event.ID.create(),
  800. created: DateTime.makeUnsafe(0),
  801. type: Bus.versionedType(DurableMessage.type, 1),
  802. seq: 0,
  803. aggregateID,
  804. data: durableData(aggregateID, "owned"),
  805. },
  806. { ownerID: "owner-1" },
  807. )
  808. const row = yield* db
  809. .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
  810. .from(EventSequenceTable)
  811. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  812. .get()
  813. .pipe(Effect.orDie)
  814. expect(row).toEqual({ seq: 0, ownerID: "owner-1" })
  815. }),
  816. )
  817. it.effect("replay claims an existing unowned sequence before fencing a different owner", () =>
  818. Effect.gen(function* () {
  819. const bus = yield* Bus.Service
  820. const { db } = yield* Database.Service
  821. const aggregateID = Session.ID.create()
  822. yield* bus.publish(DurableMessage, durableData(aggregateID, "local"))
  823. yield* bus.replay(
  824. {
  825. id: Event.ID.create(),
  826. created: DateTime.makeUnsafe(0),
  827. type: Bus.versionedType(DurableMessage.type, 1),
  828. seq: 1,
  829. aggregateID,
  830. data: durableData(aggregateID, "claimed"),
  831. },
  832. { ownerID: "owner-1" },
  833. )
  834. yield* bus.replay(
  835. {
  836. id: Event.ID.create(),
  837. created: DateTime.makeUnsafe(0),
  838. type: Bus.versionedType(DurableMessage.type, 1),
  839. seq: 2,
  840. aggregateID,
  841. data: durableData(aggregateID, "fenced"),
  842. },
  843. { ownerID: "owner-2" },
  844. )
  845. const rows = yield* db
  846. .select()
  847. .from(EventTable)
  848. .where(eq(EventTable.aggregate_id, aggregateID))
  849. .all()
  850. .pipe(Effect.orDie)
  851. const sequence = yield* db
  852. .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
  853. .from(EventSequenceTable)
  854. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  855. .get()
  856. .pipe(Effect.orDie)
  857. expect(rows.map((row) => row.seq)).toEqual([0, 1])
  858. expect(sequence).toEqual({ seq: 1, ownerID: "owner-1" })
  859. }),
  860. )
  861. it.effect("strict replay rejects an owner conflict instead of silently skipping it", () =>
  862. Effect.gen(function* () {
  863. const bus = yield* Bus.Service
  864. const aggregateID = Session.ID.create()
  865. yield* bus.replay(
  866. {
  867. id: Event.ID.create(),
  868. created: DateTime.makeUnsafe(0),
  869. type: Bus.versionedType(DurableMessage.type, 1),
  870. seq: 0,
  871. aggregateID,
  872. data: durableData(aggregateID, "claimed"),
  873. },
  874. { ownerID: "owner-1" },
  875. )
  876. const exit = yield* bus
  877. .replay(
  878. {
  879. id: Event.ID.create(),
  880. created: DateTime.makeUnsafe(0),
  881. type: Bus.versionedType(DurableMessage.type, 1),
  882. seq: 1,
  883. aggregateID,
  884. data: durableData(aggregateID, "conflict"),
  885. },
  886. { ownerID: "owner-2", strictOwner: true },
  887. )
  888. .pipe(Effect.exit)
  889. expect(String(exit)).toContain("Replay owner mismatch")
  890. }),
  891. )
  892. it.effect("publishes accepted replay with its durable sequence and suppresses stale replay", () =>
  893. Effect.gen(function* () {
  894. const bus = yield* Bus.Service
  895. const received = new Array<Event.Payload>()
  896. const aggregateID = Session.ID.create()
  897. yield* bus.listen((event) => Effect.sync(() => received.push(event)))
  898. const replayed = {
  899. id: Event.ID.create(),
  900. created: DateTime.makeUnsafe(0),
  901. type: Bus.versionedType(DurableMessage.type, 1),
  902. seq: 0,
  903. aggregateID,
  904. data: durableData(aggregateID, "replayed"),
  905. }
  906. yield* bus.replay(replayed, { publish: true })
  907. yield* bus.replay(replayed, { publish: true })
  908. expect(received).toMatchObject([{ id: replayed.id, durable: { seq: 0, version: 1 }, data: replayed.data }])
  909. }),
  910. )
  911. it.effect("rejects divergent stale replay without publishing it", () =>
  912. Effect.gen(function* () {
  913. const bus = yield* Bus.Service
  914. const received = new Array<Event.Payload>()
  915. const aggregateID = Session.ID.create()
  916. const replayed = {
  917. id: Event.ID.create(),
  918. created: DateTime.makeUnsafe(0),
  919. type: Bus.versionedType(DurableMessage.type, 1),
  920. seq: 0,
  921. aggregateID,
  922. data: durableData(aggregateID, "original"),
  923. }
  924. yield* bus.listen((event) => Effect.sync(() => received.push(event)))
  925. yield* bus.replay(replayed, { publish: true })
  926. const exit = yield* bus
  927. .replay({ ...replayed, data: durableData(aggregateID, "divergent") }, { publish: true })
  928. .pipe(Effect.exit)
  929. expect(String(exit)).toContain("Replay diverged")
  930. expect(received).toHaveLength(1)
  931. }),
  932. )
  933. it.effect("rejects an event ID reused at another aggregate position", () =>
  934. Effect.gen(function* () {
  935. const bus = yield* Bus.Service
  936. const aggregateID = Session.ID.create()
  937. const id = Event.ID.create()
  938. yield* bus.replay({
  939. id,
  940. created: DateTime.makeUnsafe(0),
  941. type: Bus.versionedType(DurableMessage.type, 1),
  942. seq: 0,
  943. aggregateID,
  944. data: durableData(aggregateID, "first"),
  945. })
  946. const exit = yield* bus
  947. .replay({
  948. id,
  949. created: DateTime.makeUnsafe(0),
  950. type: Bus.versionedType(DurableMessage.type, 1),
  951. seq: 1,
  952. aggregateID,
  953. data: durableData(aggregateID, "second"),
  954. })
  955. .pipe(Effect.exit)
  956. expect(String(exit)).toContain(`Event ${id} already exists`)
  957. }),
  958. )
  959. it.effect("replay from a different owner leaves claimed sequence unchanged", () =>
  960. Effect.gen(function* () {
  961. const bus = yield* Bus.Service
  962. const { db } = yield* Database.Service
  963. const aggregateID = Session.ID.create()
  964. const received = new Array<Event.Payload>()
  965. yield* bus.listen((event) => Effect.sync(() => received.push(event)))
  966. yield* bus.replay(
  967. {
  968. id: Event.ID.create(),
  969. created: DateTime.makeUnsafe(0),
  970. type: Bus.versionedType(DurableMessage.type, 1),
  971. seq: 0,
  972. aggregateID,
  973. data: durableData(aggregateID, "first"),
  974. },
  975. { ownerID: "owner-1" },
  976. )
  977. yield* bus.replay(
  978. {
  979. id: Event.ID.create(),
  980. created: DateTime.makeUnsafe(0),
  981. type: Bus.versionedType(DurableMessage.type, 1),
  982. seq: 1,
  983. aggregateID,
  984. data: durableData(aggregateID, "ignored"),
  985. },
  986. { ownerID: "owner-2", publish: true },
  987. )
  988. const rows = yield* db
  989. .select()
  990. .from(EventTable)
  991. .where(eq(EventTable.aggregate_id, aggregateID))
  992. .all()
  993. .pipe(Effect.orDie)
  994. const sequence = yield* db
  995. .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
  996. .from(EventSequenceTable)
  997. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  998. .get()
  999. .pipe(Effect.orDie)
  1000. expect(rows).toHaveLength(1)
  1001. expect(sequence).toEqual({ seq: 0, ownerID: "owner-1" })
  1002. expect(received).toHaveLength(0)
  1003. }),
  1004. )
  1005. it.effect("claim updates the event sequence owner", () =>
  1006. Effect.gen(function* () {
  1007. const bus = yield* Bus.Service
  1008. const { db } = yield* Database.Service
  1009. const aggregateID = Event.ID.create()
  1010. yield* bus.publish(SyncMessage, { id: aggregateID, text: "claimed" })
  1011. yield* bus.claim(aggregateID, "owner-1")
  1012. yield* bus.claim(aggregateID, "owner-2")
  1013. const row = yield* db
  1014. .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
  1015. .from(EventSequenceTable)
  1016. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  1017. .get()
  1018. .pipe(Effect.orDie)
  1019. expect(row).toEqual({ seq: 0, ownerID: "owner-2" })
  1020. }),
  1021. )
  1022. it.effect("remove clears durable event sequence", () =>
  1023. Effect.gen(function* () {
  1024. const bus = yield* Bus.Service
  1025. const received = new Array<Event.Payload>()
  1026. const aggregateID = Session.ID.create()
  1027. yield* bus.publish(DurableMessage, durableData(aggregateID, "seed"))
  1028. yield* bus.remove(aggregateID)
  1029. yield* bus.project(DurableMessage, (event) =>
  1030. Effect.sync(() => {
  1031. received.push(event)
  1032. }),
  1033. )
  1034. yield* bus.replay({
  1035. id: Event.ID.create(),
  1036. created: DateTime.makeUnsafe(0),
  1037. type: Bus.versionedType(DurableMessage.type, 1),
  1038. seq: 0,
  1039. aggregateID,
  1040. data: durableData(aggregateID, "replayed"),
  1041. })
  1042. expect(received[0]?.data).toEqual(durableData(aggregateID, "replayed"))
  1043. }),
  1044. )
  1045. it.effect("log without follow replays events and completes with a synced marker", () =>
  1046. Effect.gen(function* () {
  1047. const bus = yield* Bus.Service
  1048. const aggregateID = Session.ID.create()
  1049. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  1050. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  1051. const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID })))
  1052. expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([
  1053. Event.Seq.make(0),
  1054. Event.Seq.make(1),
  1055. "log.synced",
  1056. ])
  1057. expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(1) })
  1058. }),
  1059. )
  1060. it.effect("log synced marker omits seq for an empty log and keeps the cursor otherwise", () =>
  1061. Effect.gen(function* () {
  1062. const bus = yield* Bus.Service
  1063. const aggregateID = Session.ID.create()
  1064. const empty = Array.from(yield* Stream.runCollect(bus.log({ aggregateID })))
  1065. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  1066. const drained = Array.from(yield* Stream.runCollect(bus.log({ aggregateID, after: 0 })))
  1067. expect(empty).toEqual([{ type: "log.synced", aggregateID }])
  1068. expect(empty[0]).not.toHaveProperty("seq")
  1069. expect(drained).toEqual([{ type: "log.synced", aggregateID, seq: Event.Seq.make(0) }])
  1070. }),
  1071. )
  1072. it.effect("log with follow emits the synced marker at the replay-to-live boundary", () =>
  1073. Effect.gen(function* () {
  1074. const bus = yield* Bus.Service
  1075. const aggregateID = Session.ID.create()
  1076. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  1077. const fiber = yield* bus
  1078. .log({ aggregateID, follow: true })
  1079. .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
  1080. yield* Effect.yieldNow
  1081. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  1082. const items = Array.from(yield* Fiber.join(fiber))
  1083. expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([
  1084. Event.Seq.make(0),
  1085. { type: "log.synced", aggregateID, seq: Event.Seq.make(0) },
  1086. Event.Seq.make(1),
  1087. ])
  1088. }),
  1089. )
  1090. it.effect("log replays across configured read pages", () =>
  1091. Effect.gen(function* () {
  1092. const eventLayer = Bus.layerWith({ logReadPageSize: 2 }).pipe(Layer.provide(LayerNode.compile(Database.node)))
  1093. yield* Effect.gen(function* () {
  1094. const bus = yield* Bus.Service
  1095. const aggregateID = Session.ID.create()
  1096. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  1097. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  1098. yield* bus.publish(DurableMessage, durableData(aggregateID, "two"))
  1099. yield* bus.publish(DurableMessage, durableData(aggregateID, "three"))
  1100. yield* bus.publish(DurableMessage, durableData(aggregateID, "four"))
  1101. const items = Array.from(yield* Stream.runCollect(bus.log({ aggregateID })))
  1102. expect(items.map((item) => (Bus.isSynced(item) ? item.type : item.durable?.seq))).toEqual([
  1103. Event.Seq.make(0),
  1104. Event.Seq.make(1),
  1105. Event.Seq.make(2),
  1106. Event.Seq.make(3),
  1107. Event.Seq.make(4),
  1108. "log.synced",
  1109. ])
  1110. expect(items.at(-1)).toEqual({ type: "log.synced", aggregateID, seq: Event.Seq.make(4) })
  1111. }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer)))
  1112. }),
  1113. )
  1114. it.effect("log with follow emits events committed during replay after the synced marker", () =>
  1115. Effect.gen(function* () {
  1116. const readStarted = yield* Deferred.make<void>()
  1117. const releaseRead = yield* Deferred.make<void>()
  1118. const firstRead = yield* Ref.make(true)
  1119. const eventLayer = Bus.layerWith({
  1120. beforeAggregateRead: () =>
  1121. Ref.getAndSet(firstRead, false).pipe(
  1122. Effect.flatMap((shouldBlock) => {
  1123. if (!shouldBlock) return Effect.void
  1124. return Deferred.succeed(readStarted, undefined).pipe(Effect.andThen(Deferred.await(releaseRead)))
  1125. }),
  1126. ),
  1127. }).pipe(Layer.provide(LayerNode.compile(Database.node)))
  1128. yield* Effect.gen(function* () {
  1129. const bus = yield* Bus.Service
  1130. const aggregateID = Session.ID.create()
  1131. yield* bus.publish(DurableMessage, durableData(aggregateID, "zero"))
  1132. const fiber = yield* bus
  1133. .log({ aggregateID, follow: true })
  1134. .pipe(Stream.take(3), Stream.runCollect, Effect.forkScoped)
  1135. yield* Deferred.await(readStarted)
  1136. yield* bus.publish(DurableMessage, durableData(aggregateID, "one"))
  1137. yield* Deferred.succeed(releaseRead, undefined)
  1138. const items = Array.from(yield* Fiber.join(fiber))
  1139. expect(items.map((item) => (Bus.isSynced(item) ? item : item.durable?.seq))).toEqual([
  1140. Event.Seq.make(0),
  1141. { type: "log.synced", aggregateID, seq: Event.Seq.make(0) },
  1142. Event.Seq.make(1),
  1143. ])
  1144. }).pipe(Effect.provide(Layer.merge(LayerNode.compile(Database.node), eventLayer)))
  1145. }),
  1146. )
  1147. it.effect("sequences returns the latest committed seq per aggregate and omits unknown aggregates", () =>
  1148. Effect.gen(function* () {
  1149. const bus = yield* Bus.Service
  1150. const first = Session.ID.create()
  1151. const second = Session.ID.create()
  1152. yield* bus.publish(DurableMessage, durableData(first, "zero"))
  1153. yield* bus.publish(DurableMessage, durableData(first, "one"))
  1154. yield* bus.publish(DurableMessage, durableData(second, "zero"))
  1155. const sequences = yield* bus.sequences([first, second, Session.ID.create()])
  1156. expect(sequences).toEqual(
  1157. new Map([
  1158. [first, Event.Seq.make(1)],
  1159. [second, Event.Seq.make(0)],
  1160. ]),
  1161. )
  1162. expect(yield* bus.sequences([])).toEqual(new Map())
  1163. }),
  1164. )
  1165. })