@l-etabli/events 0.6.0 → 0.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (156) hide show
  1. package/README.md +96 -0
  2. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.cjs +54 -0
  3. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.cjs.map +1 -0
  4. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.d.cts +10 -0
  5. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.d.ts +10 -0
  6. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.mjs +30 -0
  7. package/dist/adapters/effect-kysely/EffectKyselyEventQueries.mjs.map +1 -0
  8. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.cjs +85 -0
  9. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.cjs.map +1 -0
  10. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.d.cts +9 -0
  11. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.d.ts +9 -0
  12. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.mjs +61 -0
  13. package/dist/adapters/effect-kysely/EffectKyselyEventRepository.mjs.map +1 -0
  14. package/dist/adapters/effect-kysely/index.cjs +32 -0
  15. package/dist/adapters/effect-kysely/index.cjs.map +1 -0
  16. package/dist/adapters/effect-kysely/index.d.cts +9 -0
  17. package/dist/adapters/effect-kysely/index.d.ts +9 -0
  18. package/dist/adapters/effect-kysely/index.mjs +7 -0
  19. package/dist/adapters/effect-kysely/index.mjs.map +1 -0
  20. package/dist/adapters/in-memory/InMemoryEventBus.cjs +3 -17
  21. package/dist/adapters/in-memory/InMemoryEventBus.cjs.map +1 -1
  22. package/dist/adapters/in-memory/InMemoryEventBus.mjs +2 -16
  23. package/dist/adapters/in-memory/InMemoryEventBus.mjs.map +1 -1
  24. package/dist/adapters/in-memory/InMemoryEventQueries.cjs +2 -20
  25. package/dist/adapters/in-memory/InMemoryEventQueries.cjs.map +1 -1
  26. package/dist/adapters/in-memory/InMemoryEventQueries.mjs +2 -20
  27. package/dist/adapters/in-memory/InMemoryEventQueries.mjs.map +1 -1
  28. package/dist/adapters/kysely/KyselyEventQueries.cjs +20 -24
  29. package/dist/adapters/kysely/KyselyEventQueries.cjs.map +1 -1
  30. package/dist/adapters/kysely/KyselyEventQueries.d.cts +1 -1
  31. package/dist/adapters/kysely/KyselyEventQueries.d.ts +1 -1
  32. package/dist/adapters/kysely/KyselyEventQueries.mjs +20 -24
  33. package/dist/adapters/kysely/KyselyEventQueries.mjs.map +1 -1
  34. package/dist/adapters/kysely/KyselyEventRepository.cjs +47 -45
  35. package/dist/adapters/kysely/KyselyEventRepository.cjs.map +1 -1
  36. package/dist/adapters/kysely/KyselyEventRepository.d.cts +1 -1
  37. package/dist/adapters/kysely/KyselyEventRepository.d.ts +1 -1
  38. package/dist/adapters/kysely/KyselyEventRepository.mjs +43 -41
  39. package/dist/adapters/kysely/KyselyEventRepository.mjs.map +1 -1
  40. package/dist/adapters/kysely/jsonb.cjs +30 -0
  41. package/dist/adapters/kysely/jsonb.cjs.map +1 -0
  42. package/dist/adapters/kysely/jsonb.d.cts +5 -0
  43. package/dist/adapters/kysely/jsonb.d.ts +5 -0
  44. package/dist/adapters/kysely/jsonb.mjs +6 -0
  45. package/dist/adapters/kysely/jsonb.mjs.map +1 -0
  46. package/dist/adapters/kysely/mapEventRow.cjs +35 -0
  47. package/dist/adapters/kysely/mapEventRow.cjs.map +1 -0
  48. package/dist/adapters/kysely/mapEventRow.d.cts +6 -0
  49. package/dist/adapters/kysely/mapEventRow.d.ts +6 -0
  50. package/dist/adapters/kysely/mapEventRow.mjs +11 -0
  51. package/dist/adapters/kysely/mapEventRow.mjs.map +1 -0
  52. package/dist/createEventCrawler.cjs +2 -8
  53. package/dist/createEventCrawler.cjs.map +1 -1
  54. package/dist/createEventCrawler.mjs +1 -7
  55. package/dist/createEventCrawler.mjs.map +1 -1
  56. package/dist/effect/EffectEventCrawler.cjs +111 -0
  57. package/dist/effect/EffectEventCrawler.cjs.map +1 -0
  58. package/dist/effect/EffectEventCrawler.d.cts +26 -0
  59. package/dist/effect/EffectEventCrawler.d.ts +26 -0
  60. package/dist/effect/EffectEventCrawler.mjs +87 -0
  61. package/dist/effect/EffectEventCrawler.mjs.map +1 -0
  62. package/dist/effect/EffectInMemoryEventBus.cjs +131 -0
  63. package/dist/effect/EffectInMemoryEventBus.cjs.map +1 -0
  64. package/dist/effect/EffectInMemoryEventBus.d.cts +31 -0
  65. package/dist/effect/EffectInMemoryEventBus.d.ts +31 -0
  66. package/dist/effect/EffectInMemoryEventBus.mjs +112 -0
  67. package/dist/effect/EffectInMemoryEventBus.mjs.map +1 -0
  68. package/dist/effect/EffectInMemoryEventQueries.cjs +35 -0
  69. package/dist/effect/EffectInMemoryEventQueries.cjs.map +1 -0
  70. package/dist/effect/EffectInMemoryEventQueries.d.cts +12 -0
  71. package/dist/effect/EffectInMemoryEventQueries.d.ts +12 -0
  72. package/dist/effect/EffectInMemoryEventQueries.mjs +11 -0
  73. package/dist/effect/EffectInMemoryEventQueries.mjs.map +1 -0
  74. package/dist/effect/EffectInMemoryEventRepository.cjs +73 -0
  75. package/dist/effect/EffectInMemoryEventRepository.cjs.map +1 -0
  76. package/dist/effect/EffectInMemoryEventRepository.d.cts +15 -0
  77. package/dist/effect/EffectInMemoryEventRepository.d.ts +15 -0
  78. package/dist/effect/EffectInMemoryEventRepository.mjs +48 -0
  79. package/dist/effect/EffectInMemoryEventRepository.mjs.map +1 -0
  80. package/dist/effect/EffectSubscriptions.cjs +61 -0
  81. package/dist/effect/EffectSubscriptions.cjs.map +1 -0
  82. package/dist/effect/EffectSubscriptions.d.cts +22 -0
  83. package/dist/effect/EffectSubscriptions.d.ts +22 -0
  84. package/dist/effect/EffectSubscriptions.mjs +36 -0
  85. package/dist/effect/EffectSubscriptions.mjs.map +1 -0
  86. package/dist/effect/index.cjs +47 -0
  87. package/dist/effect/index.cjs.map +1 -0
  88. package/dist/effect/index.d.cts +27 -0
  89. package/dist/effect/index.d.ts +27 -0
  90. package/dist/effect/index.mjs +20 -0
  91. package/dist/effect/index.mjs.map +1 -0
  92. package/dist/effect/ports/EffectEventBus.cjs +17 -0
  93. package/dist/effect/ports/EffectEventBus.cjs.map +1 -0
  94. package/dist/effect/ports/EffectEventBus.d.cts +13 -0
  95. package/dist/effect/ports/EffectEventBus.d.ts +13 -0
  96. package/dist/effect/ports/EffectEventBus.mjs +1 -0
  97. package/dist/effect/ports/EffectEventBus.mjs.map +1 -0
  98. package/dist/effect/ports/EffectEventQueries.cjs +17 -0
  99. package/dist/effect/ports/EffectEventQueries.cjs.map +1 -0
  100. package/dist/effect/ports/EffectEventQueries.d.cts +9 -0
  101. package/dist/effect/ports/EffectEventQueries.d.ts +9 -0
  102. package/dist/effect/ports/EffectEventQueries.mjs +1 -0
  103. package/dist/effect/ports/EffectEventQueries.mjs.map +1 -0
  104. package/dist/effect/ports/EffectEventRepository.cjs +17 -0
  105. package/dist/effect/ports/EffectEventRepository.cjs.map +1 -0
  106. package/dist/effect/ports/EffectEventRepository.d.cts +17 -0
  107. package/dist/effect/ports/EffectEventRepository.d.ts +17 -0
  108. package/dist/effect/ports/EffectEventRepository.mjs +1 -0
  109. package/dist/effect/ports/EffectEventRepository.mjs.map +1 -0
  110. package/dist/filterEvents.cjs +48 -0
  111. package/dist/filterEvents.cjs.map +1 -0
  112. package/dist/filterEvents.d.cts +6 -0
  113. package/dist/filterEvents.d.ts +6 -0
  114. package/dist/filterEvents.mjs +24 -0
  115. package/dist/filterEvents.mjs.map +1 -0
  116. package/dist/getSubscriptionIdsToPublish.cjs +40 -0
  117. package/dist/getSubscriptionIdsToPublish.cjs.map +1 -0
  118. package/dist/getSubscriptionIdsToPublish.d.cts +5 -0
  119. package/dist/getSubscriptionIdsToPublish.d.ts +5 -0
  120. package/dist/getSubscriptionIdsToPublish.mjs +16 -0
  121. package/dist/getSubscriptionIdsToPublish.mjs.map +1 -0
  122. package/dist/index.d.cts +1 -1
  123. package/dist/index.d.ts +1 -1
  124. package/dist/ports/EventQueries.cjs.map +1 -1
  125. package/dist/ports/EventQueries.d.cts +1 -1
  126. package/dist/ports/EventQueries.d.ts +1 -1
  127. package/dist/splitIntoChunks.cjs +35 -0
  128. package/dist/splitIntoChunks.cjs.map +1 -0
  129. package/dist/splitIntoChunks.d.cts +3 -0
  130. package/dist/splitIntoChunks.d.ts +3 -0
  131. package/dist/splitIntoChunks.mjs +11 -0
  132. package/dist/splitIntoChunks.mjs.map +1 -0
  133. package/package.json +18 -3
  134. package/src/adapters/effect-kysely/EffectKyselyEventQueries.ts +45 -0
  135. package/src/adapters/effect-kysely/EffectKyselyEventRepository.ts +90 -0
  136. package/src/adapters/effect-kysely/index.ts +3 -0
  137. package/src/adapters/in-memory/InMemoryEventBus.ts +2 -23
  138. package/src/adapters/in-memory/InMemoryEventQueries.ts +2 -32
  139. package/src/adapters/kysely/KyselyEventQueries.ts +27 -31
  140. package/src/adapters/kysely/KyselyEventRepository.ts +66 -64
  141. package/src/adapters/kysely/jsonb.ts +4 -0
  142. package/src/adapters/kysely/mapEventRow.ts +15 -0
  143. package/src/createEventCrawler.ts +1 -8
  144. package/src/effect/EffectEventCrawler.ts +124 -0
  145. package/src/effect/EffectInMemoryEventBus.ts +231 -0
  146. package/src/effect/EffectInMemoryEventQueries.ts +16 -0
  147. package/src/effect/EffectInMemoryEventRepository.ts +68 -0
  148. package/src/effect/EffectSubscriptions.ts +74 -0
  149. package/src/effect/index.ts +26 -0
  150. package/src/effect/ports/EffectEventBus.ts +17 -0
  151. package/src/effect/ports/EffectEventQueries.ts +9 -0
  152. package/src/effect/ports/EffectEventRepository.ts +27 -0
  153. package/src/filterEvents.ts +39 -0
  154. package/src/getSubscriptionIdsToPublish.ts +21 -0
  155. package/src/ports/EventQueries.ts +1 -1
  156. package/src/splitIntoChunks.ts +7 -0
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "The purpose of this repository is to make it easy to setup event driven architecture using outbox pattern",
4
4
  "module": "src/index.ts",
5
5
  "type": "module",
6
- "version": "0.6.0",
6
+ "version": "0.7.0",
7
7
  "main": "./dist/index.mjs",
8
8
  "types": "./dist/index.d.ts",
9
9
  "files": [
@@ -39,10 +39,11 @@
39
39
  "@commitlint/config-conventional": "^20.0.0",
40
40
  "@types/bun": "latest",
41
41
  "@types/pg": "^8.16.0",
42
+ "effect": "4.0.0-beta.43",
42
43
  "kysely": "^0.28.2",
43
44
  "lefthook": "^1.13.6",
44
45
  "pg": "^8.16.3",
45
- "semantic-release": "^24.2.9",
46
+ "semantic-release": "^25.0.3",
46
47
  "tsup": "^8.5.0",
47
48
  "typescript": "^5.9.3"
48
49
  },
@@ -56,14 +57,28 @@
56
57
  "types": "./dist/adapters/kysely/index.d.ts",
57
58
  "import": "./dist/adapters/kysely/index.mjs",
58
59
  "require": "./dist/adapters/kysely/index.cjs"
60
+ },
61
+ "./effect": {
62
+ "types": "./dist/effect/index.d.ts",
63
+ "import": "./dist/effect/index.mjs",
64
+ "require": "./dist/effect/index.cjs"
65
+ },
66
+ "./effect-kysely": {
67
+ "types": "./dist/adapters/effect-kysely/index.d.ts",
68
+ "import": "./dist/adapters/effect-kysely/index.mjs",
69
+ "require": "./dist/adapters/effect-kysely/index.cjs"
59
70
  }
60
71
  },
61
72
  "peerDependencies": {
62
- "kysely": "latest"
73
+ "kysely": ">=0.28.0",
74
+ "effect": ">=4.0.0-beta.0"
63
75
  },
64
76
  "peerDependenciesMeta": {
65
77
  "kysely": {
66
78
  "optional": true
79
+ },
80
+ "effect": {
81
+ "optional": true
67
82
  }
68
83
  }
69
84
  }
@@ -0,0 +1,45 @@
1
+ import { Effect } from "effect";
2
+ import type { Kysely, SqlBool } from "kysely";
3
+ import { sql } from "kysely";
4
+ import type { EventQueries } from "../../effect/ports/EffectEventQueries.ts";
5
+ import type { DefaultContext, GenericEvent } from "../../types.ts";
6
+ import { mapEventRow } from "../kysely/mapEventRow.ts";
7
+ import type { EventsTable } from "../kysely/types.ts";
8
+
9
+ export const createEffectKyselyEventQueries = <
10
+ Event extends GenericEvent<string, unknown, DefaultContext>,
11
+ DB extends EventsTable = EventsTable,
12
+ >(
13
+ db: Kysely<DB>,
14
+ ): EventQueries<Event> => {
15
+ const eventsDb = db as unknown as Kysely<EventsTable>;
16
+ return {
17
+ getEvents: ({ filters, limit }) =>
18
+ Effect.gen(function* () {
19
+ let query = eventsDb
20
+ .selectFrom("events")
21
+ .selectAll()
22
+ .where("status", "in", filters.statuses)
23
+ .limit(limit);
24
+
25
+ if (filters.context) {
26
+ for (const [key, value] of Object.entries(filters.context)) {
27
+ query = query.where(sql<SqlBool>`context->>${key} = ${value}`);
28
+ }
29
+ }
30
+
31
+ if (filters.occurredAt?.from) {
32
+ query = query.where("occurredAt", ">=", filters.occurredAt.from);
33
+ }
34
+
35
+ if (filters.occurredAt?.to) {
36
+ query = query.where("occurredAt", "<=", filters.occurredAt.to);
37
+ }
38
+
39
+ const rows = yield* Effect.promise(() => query.execute());
40
+ return rows.map((row: EventsTable["events"]) =>
41
+ mapEventRow<Event>(row),
42
+ );
43
+ }),
44
+ };
45
+ };
@@ -0,0 +1,90 @@
1
+ import { Effect } from "effect";
2
+ import type { Kysely } from "kysely";
3
+ import type { EventRepository } from "../../effect/ports/EffectEventRepository.ts";
4
+ import type { DefaultContext, GenericEvent } from "../../types.ts";
5
+ import { jsonb } from "../kysely/jsonb.ts";
6
+ import type { EventsTable } from "../kysely/types.ts";
7
+
8
+ export const createEffectKyselyEventRepository = <
9
+ Event extends GenericEvent<string, unknown, DefaultContext>,
10
+ DB extends EventsTable = EventsTable,
11
+ >(
12
+ db: Kysely<DB>,
13
+ ): EventRepository<Event> => {
14
+ const eventsDb = db as unknown as Kysely<EventsTable>;
15
+ return {
16
+ save: (event) =>
17
+ Effect.promise(() =>
18
+ eventsDb
19
+ .insertInto("events")
20
+ .values({
21
+ ...event,
22
+ payload: jsonb(event.payload),
23
+ triggeredByActor: jsonb(event.triggeredByActor),
24
+ context: jsonb(event.context),
25
+ publications: jsonb(event.publications),
26
+ })
27
+ .onConflict((oc) =>
28
+ oc.column("id").doUpdateSet({
29
+ topic: event.topic,
30
+ payload: jsonb(event.payload),
31
+ triggeredByActor: jsonb(event.triggeredByActor),
32
+ context: jsonb(event.context),
33
+ status: event.status,
34
+ flowId: event.flowId,
35
+ causedByEventId: event.causedByEventId,
36
+ occurredAt: event.occurredAt,
37
+ publications: jsonb(event.publications),
38
+ priority: event.priority,
39
+ }),
40
+ )
41
+ .execute(),
42
+ ).pipe(Effect.asVoid),
43
+
44
+ saveNewEventsBatch: (events) => {
45
+ if (events.length === 0) return Effect.void;
46
+ return Effect.promise(() =>
47
+ eventsDb
48
+ .insertInto("events")
49
+ .values(
50
+ events.map((event) => ({
51
+ ...event,
52
+ payload: jsonb(event.payload),
53
+ triggeredByActor: jsonb(event.triggeredByActor),
54
+ context: jsonb(event.context),
55
+ publications: jsonb(event.publications),
56
+ })),
57
+ )
58
+ .execute(),
59
+ ).pipe(Effect.asVoid);
60
+ },
61
+
62
+ markEventsAsInProcess: (events) => {
63
+ if (events.length === 0) return Effect.void;
64
+ const ids = events.map((e) => e.id);
65
+
66
+ return Effect.gen(function* () {
67
+ const lockedRows = yield* Effect.promise(() =>
68
+ eventsDb
69
+ .selectFrom("events")
70
+ .select("id")
71
+ .where("id", "in", ids)
72
+ .forUpdate()
73
+ .skipLocked()
74
+ .execute(),
75
+ );
76
+
77
+ if (lockedRows.length === 0) return;
78
+ const lockedIds = lockedRows.map((r) => r.id);
79
+
80
+ yield* Effect.promise(() =>
81
+ eventsDb
82
+ .updateTable("events")
83
+ .set({ status: "in-process" })
84
+ .where("id", "in", lockedIds)
85
+ .execute(),
86
+ ).pipe(Effect.asVoid);
87
+ });
88
+ },
89
+ };
90
+ };
@@ -0,0 +1,3 @@
1
+ export type { EventsTable, TypedEventsTable } from "../kysely/types.ts";
2
+ export { createEffectKyselyEventQueries } from "./EffectKyselyEventQueries.ts";
3
+ export { createEffectKyselyEventRepository } from "./EffectKyselyEventRepository.ts";
@@ -7,6 +7,7 @@ import type {
7
7
  EventDefinitions,
8
8
  InferEventsFromDefinitions,
9
9
  } from "../../eventDefinitions.ts";
10
+ import { getSubscriptionIdsToPublish } from "../../getSubscriptionIdsToPublish.ts";
10
11
  import type { EventBus } from "../../ports/EventBus.ts";
11
12
  import type { WithEventsUow } from "../../ports/EventRepository.ts";
12
13
  import {
@@ -113,28 +114,6 @@ export function createInMemoryEventBus<
113
114
  }
114
115
  };
115
116
 
116
- const getSubscriptionIdsToPublish = (
117
- event: Event,
118
- callbacksBySubscriptionId: SubscriptionsForTopic,
119
- ): string[] => {
120
- const allSubscriptionIds = Object.keys(callbacksBySubscriptionId);
121
-
122
- if (event.publications.length === 0 || event.status === "to-republish") {
123
- return allSubscriptionIds;
124
- }
125
-
126
- const lastPublication = event.publications.reduce((latest, current) =>
127
- current.publishedAt > latest.publishedAt ? current : latest,
128
- );
129
- const failedSubscriptionIds = (lastPublication.failures ?? []).map(
130
- (failure) => failure.subscriptionId,
131
- );
132
-
133
- return allSubscriptionIds.filter((id) =>
134
- failedSubscriptionIds.includes(id),
135
- );
136
- };
137
-
138
117
  const eventBus: EventBus<Event> = {
139
118
  publish: async (event) => {
140
119
  const publishedAt = new Date();
@@ -156,7 +135,7 @@ export function createInMemoryEventBus<
156
135
 
157
136
  const subscriptionIdsToPublish = getSubscriptionIdsToPublish(
158
137
  event,
159
- callbacksBySubscriptionSlug,
138
+ Object.keys(callbacksBySubscriptionSlug),
160
139
  );
161
140
 
162
141
  const failuresOrUndefined = await Promise.all(
@@ -1,3 +1,4 @@
1
+ import { filterEvents } from "../../filterEvents.ts";
1
2
  import type { EventQueries } from "../../ports/EventQueries.ts";
2
3
  import type { DefaultContext, GenericEvent } from "../../types.ts";
3
4
  import type { InMemoryEventRepositoryHelpers } from "./InMemoryEventRepository.ts";
@@ -8,37 +9,6 @@ export const createInMemoryEventQueries = <
8
9
  helpers: InMemoryEventRepositoryHelpers<Event>,
9
10
  ): { eventQueries: EventQueries<Event> } => ({
10
11
  eventQueries: {
11
- getEvents: async ({ filters, limit }) => {
12
- const matchesContext = (event: Event): boolean => {
13
- if (!filters.context) return true;
14
- if (!event.context) return false;
15
-
16
- return Object.entries(filters.context).every(
17
- ([key, value]) => event.context?.[key] === value,
18
- );
19
- };
20
-
21
- const matchesOccurredAt = (event: Event): boolean => {
22
- if (!filters.occurredAt) return true;
23
-
24
- const { from, to } = filters.occurredAt;
25
- const eventTime = event.occurredAt.getTime();
26
-
27
- if (from && eventTime < from.getTime()) return false;
28
- if (to && eventTime > to.getTime()) return false;
29
-
30
- return true;
31
- };
32
-
33
- return helpers
34
- .getAllEvents()
35
- .filter(
36
- (event) =>
37
- filters.statuses.includes(event.status) &&
38
- matchesContext(event) &&
39
- matchesOccurredAt(event),
40
- )
41
- .slice(0, limit);
42
- },
12
+ getEvents: async (params) => filterEvents(helpers.getAllEvents(), params),
43
13
  },
44
14
  });
@@ -2,44 +2,40 @@ import type { Kysely, SqlBool } from "kysely";
2
2
  import { sql } from "kysely";
3
3
  import type { EventQueries } from "../../ports/EventQueries.ts";
4
4
  import type { DefaultContext, GenericEvent } from "../../types.ts";
5
+ import { mapEventRow } from "./mapEventRow.ts";
5
6
  import type { EventsTable } from "./types.ts";
6
7
 
7
8
  export const createKyselyEventQueries = <
8
9
  Event extends GenericEvent<string, unknown, DefaultContext>,
10
+ DB extends EventsTable = EventsTable,
9
11
  >(
10
- db: Kysely<EventsTable>,
11
- ): EventQueries<Event> => ({
12
- getEvents: async ({ filters, limit }) => {
13
- let query = db
14
- .selectFrom("events")
15
- .selectAll()
16
- .where("status", "in", filters.statuses)
17
- .limit(limit);
12
+ db: Kysely<DB>,
13
+ ): EventQueries<Event> => {
14
+ const eventsDb = db as unknown as Kysely<EventsTable>;
15
+ return {
16
+ getEvents: async ({ filters, limit }) => {
17
+ let query = eventsDb
18
+ .selectFrom("events")
19
+ .selectAll()
20
+ .where("status", "in", filters.statuses)
21
+ .limit(limit);
18
22
 
19
- if (filters.context) {
20
- for (const [key, value] of Object.entries(filters.context)) {
21
- query = query.where(sql<SqlBool>`context->>${key} = ${value}`);
23
+ if (filters.context) {
24
+ for (const [key, value] of Object.entries(filters.context)) {
25
+ query = query.where(sql<SqlBool>`context->>${key} = ${value}`);
26
+ }
22
27
  }
23
- }
24
28
 
25
- if (filters.occurredAt?.from) {
26
- query = query.where("occurredAt", ">=", filters.occurredAt.from);
27
- }
29
+ if (filters.occurredAt?.from) {
30
+ query = query.where("occurredAt", ">=", filters.occurredAt.from);
31
+ }
28
32
 
29
- if (filters.occurredAt?.to) {
30
- query = query.where("occurredAt", "<=", filters.occurredAt.to);
31
- }
33
+ if (filters.occurredAt?.to) {
34
+ query = query.where("occurredAt", "<=", filters.occurredAt.to);
35
+ }
32
36
 
33
- const rows = await query.execute();
34
- return rows.map(
35
- (row: EventsTable["events"]) =>
36
- ({
37
- ...row,
38
- context: row.context ?? undefined,
39
- flowId: row.flowId ?? undefined,
40
- causedByEventId: row.causedByEventId ?? undefined,
41
- priority: row.priority ?? undefined,
42
- }) as Event,
43
- );
44
- },
45
- });
37
+ const rows = await query.execute();
38
+ return rows.map((row: EventsTable["events"]) => mapEventRow<Event>(row));
39
+ },
40
+ };
41
+ };
@@ -1,80 +1,82 @@
1
- import { type Kysely, type RawBuilder, sql } from "kysely";
1
+ import type { Kysely } from "kysely";
2
2
  import type { EventRepository } from "../../ports/EventRepository.ts";
3
3
  import type { DefaultContext, GenericEvent } from "../../types.ts";
4
+ import { jsonb } from "./jsonb.ts";
4
5
  import type { EventsTable } from "./types.ts";
5
6
 
6
- const jsonb = <T>(value: T): RawBuilder<T> =>
7
- sql`${JSON.stringify(value)}::jsonb`;
8
-
9
7
  export const createKyselyEventRepository = <
10
8
  Event extends GenericEvent<string, unknown, DefaultContext>,
9
+ DB extends EventsTable = EventsTable,
11
10
  >(
12
- db: Kysely<EventsTable>,
13
- ): EventRepository<Event> => ({
14
- save: async (event) => {
15
- await db
16
- .insertInto("events")
17
- .values({
18
- ...event,
19
- payload: jsonb(event.payload),
20
- triggeredByActor: jsonb(event.triggeredByActor),
21
- context: jsonb(event.context),
22
- publications: jsonb(event.publications),
23
- })
24
- .onConflict((oc) =>
25
- oc.column("id").doUpdateSet({
26
- topic: event.topic,
27
- payload: jsonb(event.payload),
28
- triggeredByActor: jsonb(event.triggeredByActor),
29
- context: jsonb(event.context),
30
- status: event.status,
31
- flowId: event.flowId,
32
- causedByEventId: event.causedByEventId,
33
- occurredAt: event.occurredAt,
34
- publications: jsonb(event.publications),
35
- priority: event.priority,
36
- }),
37
- )
38
- .execute();
39
- },
40
-
41
- saveNewEventsBatch: async (events) => {
42
- if (events.length === 0) return;
43
- await db
44
- .insertInto("events")
45
- .values(
46
- events.map((event) => ({
11
+ db: Kysely<DB>,
12
+ ): EventRepository<Event> => {
13
+ const eventsDb = db as unknown as Kysely<EventsTable>;
14
+ return {
15
+ save: async (event) => {
16
+ await eventsDb
17
+ .insertInto("events")
18
+ .values({
47
19
  ...event,
48
20
  payload: jsonb(event.payload),
49
21
  triggeredByActor: jsonb(event.triggeredByActor),
50
22
  context: jsonb(event.context),
51
23
  publications: jsonb(event.publications),
52
- })),
53
- )
54
- .execute();
55
- },
24
+ })
25
+ .onConflict((oc) =>
26
+ oc.column("id").doUpdateSet({
27
+ topic: event.topic,
28
+ payload: jsonb(event.payload),
29
+ triggeredByActor: jsonb(event.triggeredByActor),
30
+ context: jsonb(event.context),
31
+ status: event.status,
32
+ flowId: event.flowId,
33
+ causedByEventId: event.causedByEventId,
34
+ occurredAt: event.occurredAt,
35
+ publications: jsonb(event.publications),
36
+ priority: event.priority,
37
+ }),
38
+ )
39
+ .execute();
40
+ },
41
+
42
+ saveNewEventsBatch: async (events) => {
43
+ if (events.length === 0) return;
44
+ await eventsDb
45
+ .insertInto("events")
46
+ .values(
47
+ events.map((event) => ({
48
+ ...event,
49
+ payload: jsonb(event.payload),
50
+ triggeredByActor: jsonb(event.triggeredByActor),
51
+ context: jsonb(event.context),
52
+ publications: jsonb(event.publications),
53
+ })),
54
+ )
55
+ .execute();
56
+ },
56
57
 
57
- markEventsAsInProcess: async (events) => {
58
- if (events.length === 0) return;
59
- const ids = events.map((e) => e.id);
58
+ markEventsAsInProcess: async (events) => {
59
+ if (events.length === 0) return;
60
+ const ids = events.map((e) => e.id);
60
61
 
61
- // Lock the rows to prevent concurrent processing
62
- const lockedRows = await db
63
- .selectFrom("events")
64
- .select("id")
65
- .where("id", "in", ids)
66
- .forUpdate()
67
- .skipLocked()
68
- .execute();
62
+ // Lock the rows to prevent concurrent processing
63
+ const lockedRows = await eventsDb
64
+ .selectFrom("events")
65
+ .select("id")
66
+ .where("id", "in", ids)
67
+ .forUpdate()
68
+ .skipLocked()
69
+ .execute();
69
70
 
70
- if (lockedRows.length === 0) return;
71
- const lockedIds = lockedRows.map((r) => r.id);
71
+ if (lockedRows.length === 0) return;
72
+ const lockedIds = lockedRows.map((r) => r.id);
72
73
 
73
- // Update status to in-process (only for locked rows)
74
- await db
75
- .updateTable("events")
76
- .set({ status: "in-process" })
77
- .where("id", "in", lockedIds)
78
- .execute();
79
- },
80
- });
74
+ // Update status to in-process (only for locked rows)
75
+ await eventsDb
76
+ .updateTable("events")
77
+ .set({ status: "in-process" })
78
+ .where("id", "in", lockedIds)
79
+ .execute();
80
+ },
81
+ };
82
+ };
@@ -0,0 +1,4 @@
1
+ import { type RawBuilder, sql } from "kysely";
2
+
3
+ export const jsonb = <T>(value: T): RawBuilder<T> =>
4
+ sql`${JSON.stringify(value)}::jsonb`;
@@ -0,0 +1,15 @@
1
+ import type { DefaultContext, GenericEvent } from "../../types.ts";
2
+ import type { EventsTable } from "./types.ts";
3
+
4
+ export const mapEventRow = <
5
+ Event extends GenericEvent<string, unknown, DefaultContext>,
6
+ >(
7
+ row: EventsTable["events"],
8
+ ): Event =>
9
+ ({
10
+ ...row,
11
+ context: row.context ?? undefined,
12
+ flowId: row.flowId ?? undefined,
13
+ causedByEventId: row.causedByEventId ?? undefined,
14
+ priority: row.priority ?? undefined,
15
+ }) as Event;
@@ -1,6 +1,7 @@
1
1
  import type { EventBus } from "./ports/EventBus.ts";
2
2
  import type { EventQueries } from "./ports/EventQueries.ts";
3
3
  import type { WithEventsUow } from "./ports/EventRepository.ts";
4
+ import { splitIntoChunks } from "./splitIntoChunks.ts";
4
5
  import type { DefaultContext, GenericEvent } from "./types.ts";
5
6
 
6
7
  /** Configuration options for the event crawler. */
@@ -15,14 +16,6 @@ type CreateEventCrawlerOptions = {
15
16
  failedEventsIntervalMs?: number;
16
17
  };
17
18
 
18
- const splitIntoChunks = <T>(array: T[], chunkSize: number): T[][] => {
19
- const chunks: T[][] = [];
20
- for (let i = 0; i < array.length; i += chunkSize) {
21
- chunks.push(array.slice(i, i + chunkSize));
22
- }
23
- return chunks;
24
- };
25
-
26
19
  /**
27
20
  * Creates a background event crawler that processes and publishes events.
28
21
  *
@@ -0,0 +1,124 @@
1
+ import { Cause, Effect, Exit } from "effect";
2
+ import type { DefaultContext, GenericEvent } from "../types.ts";
3
+ import type { EventBus } from "./ports/EffectEventBus.ts";
4
+ import type { EventQueries } from "./ports/EffectEventQueries.ts";
5
+ import type { WithEventsUow } from "./ports/EffectEventRepository.ts";
6
+
7
+ type CreateEffectEventCrawlerOptions = {
8
+ batchSize?: number;
9
+ maxParallelProcessing?: number;
10
+ newEventsIntervalMs?: number;
11
+ failedEventsIntervalMs?: number;
12
+ };
13
+
14
+ export const createEffectEventCrawler = <
15
+ Event extends GenericEvent<string, unknown, DefaultContext>,
16
+ >({
17
+ withUow,
18
+ eventQueries,
19
+ eventBus,
20
+ options = {},
21
+ }: {
22
+ withUow: WithEventsUow<Event>;
23
+ eventQueries: EventQueries<Event>;
24
+ eventBus: EventBus<Event>;
25
+ options?: CreateEffectEventCrawlerOptions;
26
+ }) => {
27
+ const batchSize = options.batchSize ?? 100;
28
+ const maxParallelProcessing = options.maxParallelProcessing ?? 1;
29
+ const newEventsIntervalMs = options.newEventsIntervalMs ?? 10_000;
30
+ const failedEventsIntervalMs = options.failedEventsIntervalMs ?? 60_000;
31
+
32
+ const publishEventsInParallel = (events: Event[]): Effect.Effect<void> =>
33
+ Effect.all(
34
+ events.map((event) => eventBus.publish(event)),
35
+ { concurrency: maxParallelProcessing, discard: true },
36
+ );
37
+
38
+ const processNewEvents = (): Effect.Effect<void> =>
39
+ Effect.gen(function* () {
40
+ const events = yield* eventQueries.getEvents({
41
+ filters: { statuses: ["never-published"] },
42
+ limit: batchSize,
43
+ });
44
+
45
+ if (events.length === 0) return;
46
+
47
+ yield* withUow((uow) =>
48
+ uow.eventRepository.markEventsAsInProcess(events),
49
+ );
50
+
51
+ yield* publishEventsInParallel(events);
52
+ });
53
+
54
+ const retryFailedEvents = (): Effect.Effect<void> =>
55
+ Effect.gen(function* () {
56
+ const oneMinuteAgo = new Date(Date.now() - 60_000);
57
+
58
+ const events = yield* eventQueries.getEvents({
59
+ filters: {
60
+ statuses: ["to-republish", "failed-but-will-retry"],
61
+ occurredAt: { to: oneMinuteAgo },
62
+ },
63
+ limit: batchSize,
64
+ });
65
+
66
+ if (events.length === 0) return;
67
+
68
+ yield* publishEventsInParallel(events);
69
+ });
70
+
71
+ const triggerProcessing = (): Effect.Effect<void> =>
72
+ Effect.gen(function* () {
73
+ const results = yield* Effect.all(
74
+ [Effect.exit(processNewEvents()), Effect.exit(retryFailedEvents())],
75
+ { concurrency: "unbounded" },
76
+ );
77
+
78
+ const errors = results
79
+ .filter(Exit.isFailure)
80
+ .map((exit) => Cause.squash(exit.cause));
81
+
82
+ if (errors.length > 0) {
83
+ yield* Effect.die(
84
+ new AggregateError(errors, "Event processing failed"),
85
+ );
86
+ }
87
+ });
88
+
89
+ const start = () => {
90
+ const scheduleProcessNewEvents = () => {
91
+ setTimeout(async () => {
92
+ try {
93
+ await Effect.runPromise(processNewEvents());
94
+ } catch (error) {
95
+ console.error("Error processing new events:", error);
96
+ } finally {
97
+ scheduleProcessNewEvents();
98
+ }
99
+ }, newEventsIntervalMs);
100
+ };
101
+
102
+ const scheduleRetryFailedEvents = () => {
103
+ setTimeout(async () => {
104
+ try {
105
+ await Effect.runPromise(retryFailedEvents());
106
+ } catch (error) {
107
+ console.error("Error retrying failed events:", error);
108
+ } finally {
109
+ scheduleRetryFailedEvents();
110
+ }
111
+ }, failedEventsIntervalMs);
112
+ };
113
+
114
+ scheduleProcessNewEvents();
115
+ scheduleRetryFailedEvents();
116
+ };
117
+
118
+ return {
119
+ processNewEvents,
120
+ retryFailedEvents,
121
+ triggerProcessing,
122
+ start,
123
+ };
124
+ };