event.ts 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640
  1. export * as EventV2 from "./event"
  2. import { Cause, Context, Effect, Layer, Option, PubSub, Queue, Schema, Stream } from "effect"
  3. import { Event } from "@opencode-ai/schema/event"
  4. import type { Data, Definition, Payload } from "@opencode-ai/schema/event"
  5. import { and, asc, eq, gt, inArray } from "drizzle-orm"
  6. import { Database } from "./database/database"
  7. import { EventSequenceTable, EventTable } from "./event/sql"
  8. import { Location } from "./location"
  9. import { makeGlobalNode } from "./effect/node"
  10. import { isDeepStrictEqual } from "node:util"
  11. import { Durable } from "@opencode-ai/schema/durable-event-manifest"
  12. export const ID = Event.ID
  13. export type ID = import("@opencode-ai/schema/event").ID
  14. export type { Data, Definition, Payload } from "@opencode-ai/schema/event"
  15. export type Subscriber<D extends Definition = Definition> = (event: Payload<D>) => Effect.Effect<void>
  16. export type Unsubscribe = Effect.Effect<void>
  17. export const latestSequence = Effect.fn("EventV2.latestSequence")(function* (
  18. db: Database.Interface["db"],
  19. aggregateID: string,
  20. ) {
  21. const row = yield* db
  22. .select({ seq: EventSequenceTable.seq })
  23. .from(EventSequenceTable)
  24. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  25. .get()
  26. .pipe(Effect.orDie)
  27. return row?.seq ?? -1
  28. })
  29. export type SerializedEvent = {
  30. readonly id: ID
  31. readonly type: string
  32. readonly seq: number
  33. readonly aggregateID: string
  34. readonly data: Record<string, unknown>
  35. }
  36. export class InvalidDurableEventError extends Schema.TaggedErrorClass<InvalidDurableEventError>()(
  37. "EventV2.InvalidDurableEvent",
  38. {
  39. type: Schema.String,
  40. message: Schema.String,
  41. },
  42. ) {}
  43. const decodeSerializedEvent = (event: SerializedEvent): Payload => {
  44. const definition = Durable.get(event.type)
  45. if (!definition?.durable) {
  46. throw new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` })
  47. }
  48. return {
  49. id: event.id,
  50. type: definition.type,
  51. durable: { aggregateID: event.aggregateID, seq: event.seq, version: definition.durable.version },
  52. data: Schema.decodeUnknownSync(definition.data)(event.data),
  53. }
  54. }
  55. export const readAggregate = Effect.fn("EventV2.readAggregate")(function* <A>(
  56. db: Database.Interface["db"],
  57. input: {
  58. readonly aggregateID: string
  59. readonly after?: number
  60. readonly limit: number
  61. readonly manifest: {
  62. readonly definitions: ReadonlyMap<string, Definition>
  63. readonly schema: Schema.Decoder<A, never>
  64. }
  65. },
  66. ) {
  67. const after = input.after ?? -1
  68. const rows = yield* db
  69. .select()
  70. .from(EventTable)
  71. .where(
  72. and(
  73. eq(EventTable.aggregate_id, input.aggregateID),
  74. gt(EventTable.seq, after),
  75. inArray(EventTable.type, Array.from(input.manifest.definitions.keys())),
  76. ),
  77. )
  78. .orderBy(asc(EventTable.seq))
  79. .limit(input.limit + 1)
  80. .all()
  81. .pipe(Effect.orDie)
  82. const page = rows.slice(0, input.limit)
  83. const decode = Schema.decodeUnknownSync(input.manifest.schema)
  84. const events = page.map((event) =>
  85. decode({
  86. id: event.id,
  87. type: input.manifest.definitions.get(event.type)?.type ?? event.type,
  88. durable: {
  89. aggregateID: event.aggregate_id,
  90. seq: event.seq,
  91. version: input.manifest.definitions.get(event.type)?.durable?.version,
  92. },
  93. data: event.data,
  94. }),
  95. )
  96. return {
  97. events,
  98. hasMore: rows.length > input.limit,
  99. }
  100. })
  101. export class SubscriberOverflowError extends Schema.TaggedErrorClass<SubscriberOverflowError>()(
  102. "EventV2.SubscriberOverflow",
  103. { capacity: Schema.Int },
  104. ) {}
  105. export const define = Event.define
  106. export const versionedType = Event.versionedType
  107. export interface PublishOptions {
  108. readonly id?: ID
  109. readonly metadata?: Record<string, unknown>
  110. readonly location?: Location.Ref
  111. /** Local operational projection committed atomically with a new durable event. Not replayed or serialized. */
  112. readonly commit?: (seq: number) => Effect.Effect<void>
  113. }
  114. export interface Interface {
  115. readonly publish: <D extends Definition>(
  116. definition: D,
  117. data: Data<D>,
  118. options?: PublishOptions,
  119. ) => Effect.Effect<Payload<D>>
  120. readonly subscribe: <D extends Definition>(definition: D) => Stream.Stream<Payload<D>>
  121. readonly all: () => Stream.Stream<Payload>
  122. readonly durable: (input: { readonly aggregateID: string; readonly after?: number }) => Stream.Stream<Payload>
  123. /** @deprecated Use `all()` and consume the returned stream. */
  124. readonly listen: (listener: Subscriber) => Effect.Effect<Unsubscribe>
  125. readonly project: <D extends Definition>(definition: D, projector: Subscriber<D>) => Effect.Effect<void>
  126. readonly replay: (
  127. event: SerializedEvent,
  128. options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
  129. ) => Effect.Effect<void>
  130. readonly replayAll: (
  131. events: SerializedEvent[],
  132. options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
  133. ) => Effect.Effect<string | undefined>
  134. readonly remove: (aggregateID: string) => Effect.Effect<void>
  135. readonly claim: (aggregateID: string, ownerID: string) => Effect.Effect<void>
  136. }
  137. export class Service extends Context.Service<Service, Interface>()("@opencode/Event") {}
  138. export const allBounded = (events: Interface, capacity: number) =>
  139. Effect.gen(function* () {
  140. const queue = yield* Queue.dropping<Payload, SubscriberOverflowError>(capacity)
  141. const unsubscribe = yield* events.listen((event) =>
  142. Queue.offer(queue, event).pipe(
  143. Effect.flatMap((accepted) =>
  144. accepted ? Effect.void : Queue.fail(queue, new SubscriberOverflowError({ capacity })).pipe(Effect.asVoid),
  145. ),
  146. ),
  147. )
  148. yield* Effect.addFinalizer(() => unsubscribe.pipe(Effect.andThen(Queue.shutdown(queue)), Effect.asVoid))
  149. return Stream.fromQueue(queue)
  150. })
  151. export interface LayerOptions {
  152. readonly beforeAggregateRead?: (aggregateID: string) => Effect.Effect<void>
  153. }
  154. export const layerWith = (options?: LayerOptions) =>
  155. Layer.effect(
  156. Service,
  157. Effect.gen(function* () {
  158. const pubsub = {
  159. all: yield* PubSub.unbounded<Payload>(),
  160. durable: new Map<string, Set<PubSub.PubSub<void>>>(),
  161. typed: new Map<string, PubSub.PubSub<Payload>>(),
  162. }
  163. const projectors = new Map<string, Subscriber[]>()
  164. // TODO: Bind durable projectors to exact type+version before supporting incompatible historical payloads.
  165. const listeners = new Array<Subscriber>()
  166. const { db } = yield* Database.Service
  167. const getOrCreate = (definition: Definition) =>
  168. Effect.gen(function* () {
  169. const existing = pubsub.typed.get(definition.type)
  170. if (existing) return existing
  171. const created = yield* PubSub.unbounded<Payload>()
  172. pubsub.typed.set(definition.type, created)
  173. return created
  174. })
  175. yield* Effect.addFinalizer(() =>
  176. Effect.gen(function* () {
  177. yield* PubSub.shutdown(pubsub.all)
  178. yield* Effect.forEach(
  179. pubsub.durable.values(),
  180. (pubsubs) => Effect.forEach(pubsubs, PubSub.shutdown, { discard: true }),
  181. { discard: true },
  182. )
  183. yield* Effect.forEach(pubsub.typed.values(), PubSub.shutdown, { discard: true })
  184. }),
  185. )
  186. function commitDurableEvent(
  187. definition: Definition,
  188. event: Payload,
  189. input?: {
  190. readonly seq: number
  191. readonly aggregateID: string
  192. readonly ownerID?: string
  193. readonly strictOwner?: boolean
  194. },
  195. commit?: (seq: number) => Effect.Effect<void>,
  196. ) {
  197. return Effect.gen(function* () {
  198. const durable = definition?.durable
  199. if (durable) {
  200. const aggregateID = (event.data as Record<string, unknown>)[durable.aggregate]
  201. if (typeof aggregateID !== "string") {
  202. yield* Effect.die(
  203. new InvalidDurableEventError({
  204. type: event.type,
  205. message: `Expected string aggregate field ${durable.aggregate}`,
  206. }),
  207. )
  208. } else {
  209. if (input && input.aggregateID !== aggregateID) {
  210. yield* Effect.die(
  211. new InvalidDurableEventError({
  212. type: event.type,
  213. message: `Aggregate mismatch: expected ${input.aggregateID}, got ${aggregateID}`,
  214. }),
  215. )
  216. }
  217. const list = projectors.get(event.type) ?? []
  218. return yield* Effect.uninterruptible(
  219. Effect.gen(function* () {
  220. const committed = yield* db
  221. .transaction(
  222. () =>
  223. Effect.gen(function* () {
  224. const row = yield* db
  225. .select({ seq: EventSequenceTable.seq, ownerID: EventSequenceTable.owner_id })
  226. .from(EventSequenceTable)
  227. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  228. .get()
  229. .pipe(Effect.orDie)
  230. const latest = row?.seq ?? -1
  231. const encoded = Schema.encodeUnknownSync(definition.data)(event.data) as Record<
  232. string,
  233. unknown
  234. >
  235. if (input?.strictOwner && row?.ownerID && row.ownerID !== input.ownerID) {
  236. yield* Effect.die(
  237. new InvalidDurableEventError({
  238. type: event.type,
  239. message: `Replay owner mismatch for aggregate ${aggregateID}: expected ${row.ownerID}, got ${input.ownerID ?? "none"}`,
  240. }),
  241. )
  242. }
  243. if (input && input.seq <= latest) {
  244. const stored = yield* db
  245. .select()
  246. .from(EventTable)
  247. .where(and(eq(EventTable.aggregate_id, aggregateID), eq(EventTable.seq, input.seq)))
  248. .get()
  249. .pipe(Effect.orDie)
  250. if (
  251. stored?.id === event.id &&
  252. stored.type === versionedType(definition.type, durable.version) &&
  253. isDeepStrictEqual(stored.data, encoded)
  254. ) {
  255. if (input.ownerID && row?.ownerID == null) {
  256. yield* db
  257. .update(EventSequenceTable)
  258. .set({ owner_id: input.ownerID })
  259. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  260. .run()
  261. .pipe(Effect.orDie)
  262. }
  263. return
  264. }
  265. yield* Effect.die(
  266. new InvalidDurableEventError({
  267. type: event.type,
  268. message: `Replay diverged at aggregate ${aggregateID} sequence ${input.seq}`,
  269. }),
  270. )
  271. }
  272. if (input && row?.ownerID && row.ownerID !== input.ownerID) {
  273. return
  274. }
  275. const seq = input?.seq ?? latest + 1
  276. if (input && seq !== latest + 1) {
  277. yield* Effect.die(
  278. new InvalidDurableEventError({
  279. type: event.type,
  280. message: `Sequence mismatch for aggregate ${aggregateID}: expected ${latest + 1}, got ${seq}`,
  281. }),
  282. )
  283. }
  284. const stored = yield* db
  285. .select({ aggregateID: EventTable.aggregate_id, seq: EventTable.seq })
  286. .from(EventTable)
  287. .where(eq(EventTable.id, event.id))
  288. .get()
  289. .pipe(Effect.orDie)
  290. if (stored)
  291. yield* Effect.die(
  292. new InvalidDurableEventError({
  293. type: event.type,
  294. message: `Event ${event.id} already exists at aggregate ${stored.aggregateID} sequence ${stored.seq}`,
  295. }),
  296. )
  297. const committed = {
  298. ...event,
  299. durable: { aggregateID, seq, version: durable.version },
  300. } as Payload
  301. for (const projector of list) {
  302. yield* projector(committed)
  303. }
  304. if (commit) yield* commit(seq)
  305. yield* db
  306. .insert(EventSequenceTable)
  307. .values([{ aggregate_id: aggregateID, seq, owner_id: input?.ownerID }])
  308. .onConflictDoUpdate({
  309. target: EventSequenceTable.aggregate_id,
  310. set: {
  311. seq,
  312. ...(input?.ownerID && row?.ownerID == null ? { owner_id: input.ownerID } : {}),
  313. },
  314. })
  315. .run()
  316. .pipe(Effect.orDie)
  317. yield* db
  318. .insert(EventTable)
  319. .values([
  320. {
  321. id: event.id,
  322. aggregate_id: aggregateID,
  323. seq,
  324. type: versionedType(definition.type, durable.version),
  325. data: encoded,
  326. },
  327. ])
  328. .run()
  329. .pipe(Effect.orDie)
  330. return { aggregateID, seq }
  331. }),
  332. { behavior: "immediate" },
  333. )
  334. .pipe(Effect.orDie)
  335. if (committed) {
  336. yield* Effect.forEach(
  337. pubsub.durable.get(committed.aggregateID) ?? [],
  338. (wake) => PubSub.publish(wake, undefined),
  339. { discard: true },
  340. )
  341. }
  342. return committed
  343. }),
  344. )
  345. }
  346. }
  347. })
  348. }
  349. function publishEvent<D extends Definition>(definition: D, event: Payload<D>, commit?: PublishOptions["commit"]) {
  350. return Effect.gen(function* () {
  351. if (!definition?.durable && commit)
  352. return yield* Effect.die(
  353. new InvalidDurableEventError({
  354. type: event.type,
  355. message: "Local commit hooks require a durable event",
  356. }),
  357. )
  358. if (definition?.durable) {
  359. const committed = yield* commitDurableEvent(definition, event as Payload, undefined, commit)
  360. if (committed) {
  361. event = {
  362. ...event,
  363. durable: {
  364. aggregateID: committed.aggregateID,
  365. seq: committed.seq,
  366. version: definition.durable.version,
  367. },
  368. }
  369. yield* notify(event as Payload, true)
  370. return event
  371. }
  372. }
  373. yield* notify(event as Payload, false)
  374. return event
  375. })
  376. }
  377. const observe = (event: Payload, observer: (event: Payload) => Effect.Effect<void>) =>
  378. Effect.suspend(() => observer(event)).pipe(
  379. Effect.catchCauseIf(
  380. (cause) => !Cause.hasInterrupts(cause),
  381. (cause) => Effect.logError("Event listener failed", { eventID: event.id, eventType: event.type, cause }),
  382. ),
  383. )
  384. function notify(event: Payload, isolateListeners: boolean) {
  385. return Effect.gen(function* () {
  386. yield* Effect.forEach(
  387. listeners,
  388. (listener) => (isolateListeners ? observe(event, listener) : listener(event)),
  389. { discard: true },
  390. )
  391. const typed = pubsub.typed.get(event.type)
  392. if (typed) yield* PubSub.publish(typed, event)
  393. yield* PubSub.publish(pubsub.all, event)
  394. })
  395. }
  396. function publish<D extends Definition>(definition: D, data: Data<D>, options?: PublishOptions) {
  397. return Effect.gen(function* () {
  398. const serviceLocation = Option.getOrUndefined(yield* Effect.serviceOption(Location.Service))
  399. const location =
  400. options?.location ??
  401. (serviceLocation
  402. ? { directory: serviceLocation.directory, workspaceID: serviceLocation.workspaceID }
  403. : undefined)
  404. return yield* publishEvent(
  405. definition,
  406. {
  407. id: options?.id ?? ID.create(),
  408. ...(options?.metadata ? { metadata: options.metadata } : {}),
  409. type: definition.type,
  410. ...(location ? { location } : {}),
  411. data,
  412. } as Payload<D>,
  413. options?.commit,
  414. )
  415. })
  416. }
  417. function replay(
  418. event: SerializedEvent,
  419. options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
  420. ) {
  421. return Effect.gen(function* () {
  422. const definition = Durable.get(event.type)
  423. if (!definition?.durable) {
  424. yield* Effect.die(
  425. new InvalidDurableEventError({ type: event.type, message: `Unknown durable event type ${event.type}` }),
  426. )
  427. } else {
  428. const payload = {
  429. id: event.id,
  430. type: definition.type,
  431. data: Schema.decodeUnknownSync(definition.data)(event.data),
  432. } as Payload
  433. const committed = yield* commitDurableEvent(definition, payload, {
  434. seq: event.seq,
  435. aggregateID: event.aggregateID,
  436. ownerID: options?.ownerID,
  437. strictOwner: options?.strictOwner,
  438. })
  439. if (committed && options?.publish) {
  440. yield* notify(
  441. {
  442. ...payload,
  443. durable: {
  444. aggregateID: committed.aggregateID,
  445. seq: committed.seq,
  446. version: definition.durable.version,
  447. },
  448. },
  449. true,
  450. )
  451. }
  452. }
  453. })
  454. }
  455. function replayAll(
  456. events: SerializedEvent[],
  457. options?: { readonly publish?: boolean; readonly ownerID?: string; readonly strictOwner?: boolean },
  458. ) {
  459. return Effect.gen(function* () {
  460. const source = events[0]?.aggregateID
  461. if (!source) return undefined
  462. if (events.some((event) => event.aggregateID !== source)) {
  463. yield* Effect.die(
  464. new InvalidDurableEventError({
  465. type: events[0]?.type ?? "unknown",
  466. message: "Replay events must belong to the same aggregate",
  467. }),
  468. )
  469. }
  470. const start = events[0]?.seq ?? 0
  471. for (const [index, event] of events.entries()) {
  472. const seq = start + index
  473. if (event.seq !== seq) {
  474. yield* Effect.die(
  475. new InvalidDurableEventError({
  476. type: event.type,
  477. message: `Replay sequence mismatch at index ${index}: expected ${seq}, got ${event.seq}`,
  478. }),
  479. )
  480. }
  481. }
  482. for (const event of events) {
  483. yield* replay(event, options)
  484. }
  485. return source
  486. })
  487. }
  488. function remove(aggregateID: string) {
  489. return db
  490. .transaction(() =>
  491. Effect.gen(function* () {
  492. yield* db.delete(EventSequenceTable).where(eq(EventSequenceTable.aggregate_id, aggregateID)).run()
  493. yield* db.delete(EventTable).where(eq(EventTable.aggregate_id, aggregateID)).run()
  494. }),
  495. )
  496. .pipe(Effect.orDie)
  497. }
  498. function claim(aggregateID: string, ownerID: string) {
  499. return db
  500. .update(EventSequenceTable)
  501. .set({ owner_id: ownerID })
  502. .where(eq(EventSequenceTable.aggregate_id, aggregateID))
  503. .run()
  504. .pipe(Effect.orDie)
  505. }
  506. const subscribe = <D extends Definition>(definition: D): Stream.Stream<Payload<D>> =>
  507. Stream.unwrap(getOrCreate(definition).pipe(Effect.map((pubsub) => Stream.fromPubSub(pubsub)))).pipe(
  508. Stream.map((event) => event as Payload<D>),
  509. )
  510. const streamAll = (): Stream.Stream<Payload> => Stream.fromPubSub(pubsub.all)
  511. const readAfter = (aggregateID: string, after: number) =>
  512. (options?.beforeAggregateRead?.(aggregateID) ?? Effect.void).pipe(
  513. Effect.andThen(
  514. db
  515. .select()
  516. .from(EventTable)
  517. .where(and(eq(EventTable.aggregate_id, aggregateID), gt(EventTable.seq, after)))
  518. .orderBy(asc(EventTable.seq))
  519. .all(),
  520. ),
  521. Effect.orDie,
  522. Effect.map((rows) =>
  523. rows.map((event) =>
  524. decodeSerializedEvent({
  525. id: event.id,
  526. aggregateID: event.aggregate_id,
  527. seq: event.seq,
  528. type: event.type,
  529. data: event.data,
  530. }),
  531. ),
  532. ),
  533. )
  534. const subscribeDurable = (aggregateID: string) =>
  535. Effect.gen(function* () {
  536. const wake = yield* PubSub.sliding<void>(1)
  537. const subscription = yield* PubSub.subscribe(wake)
  538. yield* Effect.acquireRelease(
  539. Effect.sync(() => {
  540. const wakes = pubsub.durable.get(aggregateID) ?? new Set()
  541. wakes.add(wake)
  542. pubsub.durable.set(aggregateID, wakes)
  543. }),
  544. () =>
  545. Effect.sync(() => {
  546. const wakes = pubsub.durable.get(aggregateID)
  547. wakes?.delete(wake)
  548. if (wakes?.size === 0) pubsub.durable.delete(aggregateID)
  549. }).pipe(Effect.andThen(PubSub.shutdown(wake))),
  550. )
  551. return subscription
  552. })
  553. const durable = (input: { readonly aggregateID: string; readonly after?: number }): Stream.Stream<Payload> =>
  554. Stream.unwrap(
  555. Effect.gen(function* () {
  556. const wakes = yield* subscribeDurable(input.aggregateID)
  557. let sequence = input.after ?? -1
  558. const read = Effect.suspend(() => readAfter(input.aggregateID, sequence)).pipe(
  559. Effect.tap((events) =>
  560. Effect.sync(() => {
  561. sequence = events.at(-1)?.durable?.seq ?? sequence
  562. }),
  563. ),
  564. )
  565. const historical = yield* read
  566. const live = Stream.fromSubscription(wakes).pipe(
  567. Stream.mapEffect(() => read),
  568. Stream.flattenIterable,
  569. )
  570. return Stream.concat(Stream.fromIterable(historical), live)
  571. }),
  572. )
  573. const listen = (listener: Subscriber): Effect.Effect<Unsubscribe> =>
  574. Effect.sync(() => {
  575. listeners.push(listener)
  576. return Effect.sync(() => {
  577. const index = listeners.indexOf(listener)
  578. if (index >= 0) listeners.splice(index, 1)
  579. })
  580. })
  581. const project = <D extends Definition>(definition: D, projector: Subscriber<D>): Effect.Effect<void> =>
  582. Effect.sync(() => {
  583. const list = projectors.get(definition.type) ?? []
  584. list.push((event) => projector(event as Payload<D>))
  585. projectors.set(definition.type, list)
  586. })
  587. return Service.of({
  588. publish,
  589. subscribe,
  590. all: streamAll,
  591. durable,
  592. listen,
  593. project,
  594. replay,
  595. replayAll,
  596. remove,
  597. claim,
  598. })
  599. }),
  600. )
  601. export const layer = layerWith()
  602. export const node = makeGlobalNode({ service: Service, layer: layer, deps: [Database.node] })
  603. export const defaultLayer = layer.pipe(Layer.provide(Database.defaultLayer))